add pull requests view with PR and patch panels
Rust / build (macos-latest, stable) (push) Waiting to run
Rust / build (ubuntu-latest, stable) (push) Waiting to run
Rust / build (windows-latest, stable) (push) Waiting to run
Rust / build (macos-latest, stable) (pull_request) Waiting to run
Rust / build (ubuntu-latest, stable) (pull_request) Waiting to run
Rust / build (windows-latest, stable) (pull_request) Waiting to run
Rust / build (macos-latest, stable) (push) Waiting to run
Rust / build (ubuntu-latest, stable) (push) Waiting to run
Rust / build (windows-latest, stable) (push) Waiting to run
Rust / build (macos-latest, stable) (pull_request) Waiting to run
Rust / build (ubuntu-latest, stable) (pull_request) Waiting to run
Rust / build (windows-latest, stable) (pull_request) Waiting to run
This commit is contained in:
@@ -96,4 +96,3 @@ impl IconNamed for CustomIconName {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ impl ExploreView {
|
|||||||
let store = RepoListStore::global(cx);
|
let store = RepoListStore::global(cx);
|
||||||
let directory = ProfileDirectory::new(cx);
|
let directory = ProfileDirectory::new(cx);
|
||||||
|
|
||||||
let entity = cx.entity();
|
let weak = cx.entity().downgrade();
|
||||||
let search = InputState::new(cx);
|
let search = InputState::new(cx);
|
||||||
|
|
||||||
let search_subscription = cx.subscribe(&search, |this, _search, event, cx| {
|
let search_subscription = cx.subscribe(&search, |this, _search, event, cx| {
|
||||||
@@ -56,9 +56,9 @@ impl ExploreView {
|
|||||||
});
|
});
|
||||||
|
|
||||||
cx.defer(move |cx| {
|
cx.defer(move |cx| {
|
||||||
entity.update(cx, |this, cx| {
|
if let Err(error) = weak.update(cx, |this, cx| this.rebuild_rows(cx)) {
|
||||||
this.rebuild_rows(cx);
|
log::warn!("failed to load explore rows: {error}");
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -39,17 +39,19 @@ impl IssueDetailView {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let directory = ProfileDirectory::new(cx);
|
let directory = ProfileDirectory::new(cx);
|
||||||
|
let weak = cx.entity().downgrade();
|
||||||
|
cx.defer(move |cx| {
|
||||||
|
let _ = weak.update(cx, |this, cx| this.track_participants(cx));
|
||||||
|
});
|
||||||
|
|
||||||
let mut view = Self {
|
Self {
|
||||||
store,
|
store,
|
||||||
issue_id,
|
issue_id,
|
||||||
comment_input,
|
comment_input,
|
||||||
directory,
|
directory,
|
||||||
scroll_handle: ScrollHandle::new(),
|
scroll_handle: ScrollHandle::new(),
|
||||||
_subscription: subscription,
|
_subscription: subscription,
|
||||||
};
|
}
|
||||||
view.track_participants(cx);
|
|
||||||
view
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Track the issue author, everyone who commented and the repository owner.
|
/// Track the issue author, everyone who commented and the repository owner.
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ use signed_state::RepoStore;
|
|||||||
use utils::relative_time;
|
use utils::relative_time;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub(super) enum IssueFilter {
|
pub(crate) enum IssueFilter {
|
||||||
All,
|
All,
|
||||||
Open,
|
Open,
|
||||||
Closed,
|
Closed,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IssueFilter {
|
impl IssueFilter {
|
||||||
pub(super) fn matches(self, status: RepoStatus) -> bool {
|
pub(crate) fn matches(self, status: RepoStatus) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::All => true,
|
Self::All => true,
|
||||||
Self::Open => status == RepoStatus::Open,
|
Self::Open => status == RepoStatus::Open,
|
||||||
@@ -36,12 +36,12 @@ impl IssueFilter {
|
|||||||
|
|
||||||
/// Root events counted by their resolved status.
|
/// Root events counted by their resolved status.
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
pub(super) struct StatusCounts {
|
pub(crate) struct StatusCounts {
|
||||||
pub(super) total: usize,
|
pub(crate) total: usize,
|
||||||
pub(super) open: usize,
|
pub(crate) open: usize,
|
||||||
pub(super) closed: usize,
|
pub(crate) closed: usize,
|
||||||
pub(super) draft: usize,
|
pub(crate) draft: usize,
|
||||||
pub(super) applied: usize,
|
pub(crate) applied: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StatusCounts {
|
impl StatusCounts {
|
||||||
@@ -57,7 +57,7 @@ impl StatusCounts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Indices of `roots` whose status `keep` accepts, counting every root's status.
|
/// Indices of `roots` whose status `keep` accepts, counting every root's status.
|
||||||
pub(super) fn filter_by_status<'a>(
|
pub(crate) fn filter_by_status<'a>(
|
||||||
roots: impl IntoIterator<Item = &'a Event>,
|
roots: impl IntoIterator<Item = &'a Event>,
|
||||||
status_of: impl Fn(&Event) -> RepoStatus,
|
status_of: impl Fn(&Event) -> RepoStatus,
|
||||||
keep: impl Fn(RepoStatus) -> bool,
|
keep: impl Fn(RepoStatus) -> bool,
|
||||||
@@ -77,11 +77,15 @@ pub(super) fn filter_by_status<'a>(
|
|||||||
(visible, counts)
|
(visible, counts)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn issue_roots(store: &RepoStore) -> &[Event] {
|
pub(crate) fn issue_roots(store: &RepoStore) -> &[Event] {
|
||||||
&store.issues
|
&store.issues
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn sidebar_section(
|
pub(crate) fn pr_roots(store: &RepoStore) -> &[Event] {
|
||||||
|
&store.pull_requests
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sidebar_section(
|
||||||
store: &Entity<RepoStore>,
|
store: &Entity<RepoStore>,
|
||||||
id: EventId,
|
id: EventId,
|
||||||
roots: fn(&RepoStore) -> &[Event],
|
roots: fn(&RepoStore) -> &[Event],
|
||||||
@@ -153,7 +157,7 @@ pub(super) fn sidebar_section(
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn comments_section(
|
pub(crate) fn comments_section(
|
||||||
store: &Entity<RepoStore>,
|
store: &Entity<RepoStore>,
|
||||||
root: EventId,
|
root: EventId,
|
||||||
directory: &Entity<ProfileDirectory>,
|
directory: &Entity<ProfileDirectory>,
|
||||||
@@ -214,7 +218,7 @@ pub(super) fn comments_section(
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn comment_form(
|
pub(crate) fn comment_form(
|
||||||
store: &Entity<RepoStore>,
|
store: &Entity<RepoStore>,
|
||||||
root: EventId,
|
root: EventId,
|
||||||
roots: fn(&RepoStore) -> &[Event],
|
roots: fn(&RepoStore) -> &[Event],
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ use crate::shared::panels;
|
|||||||
use crate::shared::widgets::{repo_tab_avatar, status_badge};
|
use crate::shared::widgets::{repo_tab_avatar, status_badge};
|
||||||
|
|
||||||
pub mod detail;
|
pub mod detail;
|
||||||
mod helpers;
|
pub(crate) mod helpers;
|
||||||
pub mod new_issue;
|
pub mod new_issue;
|
||||||
|
|
||||||
const ISSUE_ROW_HEIGHT: f32 = 73.;
|
const ISSUE_ROW_HEIGHT: f32 = 73.;
|
||||||
@@ -58,8 +58,12 @@ impl IssuesView {
|
|||||||
let subscription = cx.observe(&store, |this, _store, cx| {
|
let subscription = cx.observe(&store, |this, _store, cx| {
|
||||||
this.rebuild(cx);
|
this.rebuild(cx);
|
||||||
});
|
});
|
||||||
|
let weak = cx.entity().downgrade();
|
||||||
|
cx.defer(move |cx| {
|
||||||
|
let _ = weak.update(cx, |this, cx| this.rebuild(cx));
|
||||||
|
});
|
||||||
|
|
||||||
let mut view = Self {
|
Self {
|
||||||
dock,
|
dock,
|
||||||
store,
|
store,
|
||||||
repo_name,
|
repo_name,
|
||||||
@@ -70,9 +74,7 @@ impl IssuesView {
|
|||||||
scroll_handle: UniformListScrollHandle::new(),
|
scroll_handle: UniformListScrollHandle::new(),
|
||||||
directory: ProfileDirectory::new(cx),
|
directory: ProfileDirectory::new(cx),
|
||||||
_subscription: subscription,
|
_subscription: subscription,
|
||||||
};
|
}
|
||||||
view.rebuild(cx);
|
|
||||||
view
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rebuild(&mut self, cx: &mut Context<Self>) {
|
fn rebuild(&mut self, cx: &mut Context<Self>) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ mod app;
|
|||||||
mod explore;
|
mod explore;
|
||||||
mod identity;
|
mod identity;
|
||||||
mod issues;
|
mod issues;
|
||||||
|
mod pull_requests;
|
||||||
mod repo;
|
mod repo;
|
||||||
mod shared;
|
mod shared;
|
||||||
mod sidebar;
|
mod sidebar;
|
||||||
|
|||||||
@@ -0,0 +1,628 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use assets::CustomIconName;
|
||||||
|
use gpui::prelude::FluentBuilder as _;
|
||||||
|
use gpui::{
|
||||||
|
AnyElement, App, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement,
|
||||||
|
ParentElement as _, Render, SharedString, Styled as _, Subscription, UniformListScrollHandle,
|
||||||
|
WeakEntity, Window, div, uniform_list,
|
||||||
|
};
|
||||||
|
use gpui_native_kit::avatar::Avatar;
|
||||||
|
use gpui_native_kit::button::Button;
|
||||||
|
use gpui_native_kit::clipboard::ClipboardButton;
|
||||||
|
use gpui_native_kit::common::{Sizable as _, h_flex, v_flex};
|
||||||
|
use gpui_native_kit::data::tabs::{Tab, Tabs};
|
||||||
|
use gpui_native_kit::icons::Icon;
|
||||||
|
use gpui_native_kit::input::InputState;
|
||||||
|
use gpui_native_kit::layout::dock::{DockState, Panel};
|
||||||
|
use gpui_native_kit::scroll_area::ScrollArea;
|
||||||
|
use gpui_native_kit::scrollbar::Scrollbar;
|
||||||
|
use gpui_native_kit::spinner::Spinner;
|
||||||
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
|
use metadata::ProfileDirectory;
|
||||||
|
use nostr::prelude::{EventId, Kind, PublicKey, Url};
|
||||||
|
use signed_core::{GitEvent, PullRequest, RepoAddr};
|
||||||
|
use signed_git::{FileCommit, PatchParser};
|
||||||
|
use signed_state::{Backend, Mirrors, RepoStore};
|
||||||
|
use utils::relative_time;
|
||||||
|
|
||||||
|
use super::dialogs;
|
||||||
|
use crate::issues::helpers::{comment_form, comments_section, pr_roots, sidebar_section};
|
||||||
|
use crate::repo::commit_diff::{CommitDiffView, DiffPane};
|
||||||
|
use crate::repo::helpers::commit_row;
|
||||||
|
use crate::shared::widgets::{empty_state, repo_tab_avatar, status_badge};
|
||||||
|
|
||||||
|
const NOT_FOUND: &str = "Pull request not found";
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
|
struct PrBinding {
|
||||||
|
description: String,
|
||||||
|
patch: String,
|
||||||
|
tip: Option<String>,
|
||||||
|
base: Option<String>,
|
||||||
|
clone_urls: Vec<Url>,
|
||||||
|
addr: RepoAddr,
|
||||||
|
has_patch_link: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PullRequestDetailView {
|
||||||
|
dock: WeakEntity<DockState>,
|
||||||
|
store: Entity<RepoStore>,
|
||||||
|
pr_id: EventId,
|
||||||
|
comment_input: Entity<InputState>,
|
||||||
|
repo_name: SharedString,
|
||||||
|
worktree: Option<PathBuf>,
|
||||||
|
description: SharedString,
|
||||||
|
current_commit: Option<SharedString>,
|
||||||
|
commits: Vec<FileCommit>,
|
||||||
|
loading: bool,
|
||||||
|
error: Option<SharedString>,
|
||||||
|
bound: Option<PrBinding>,
|
||||||
|
load_generation: u64,
|
||||||
|
active_tab: usize,
|
||||||
|
pane: Entity<DiffPane>,
|
||||||
|
commit_scroll_handle: UniformListScrollHandle,
|
||||||
|
tasks: Vec<gpui::Task<Result<(), anyhow::Error>>>,
|
||||||
|
directory: Entity<ProfileDirectory>,
|
||||||
|
_subscription: Subscription,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PullRequestDetailView {
|
||||||
|
pub fn new(
|
||||||
|
dock: WeakEntity<DockState>,
|
||||||
|
store: Entity<RepoStore>,
|
||||||
|
pr_id: EventId,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let pane = cx.new(DiffPane::new);
|
||||||
|
|
||||||
|
let comment_input = InputState::new(cx);
|
||||||
|
comment_input.update(cx, |state, cx| state.set_multiline(true, cx));
|
||||||
|
|
||||||
|
let subscription = cx.observe(&store, |this, _store, cx| {
|
||||||
|
this.sync(cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
let weak = cx.entity().downgrade();
|
||||||
|
let repo_name = store.read(cx).name();
|
||||||
|
|
||||||
|
cx.defer(move |cx| {
|
||||||
|
let _ = weak.update(cx, |this, cx| this.sync(cx));
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
dock,
|
||||||
|
store,
|
||||||
|
pr_id,
|
||||||
|
comment_input,
|
||||||
|
repo_name,
|
||||||
|
worktree: None,
|
||||||
|
description: SharedString::default(),
|
||||||
|
current_commit: None,
|
||||||
|
commits: Vec::new(),
|
||||||
|
loading: true,
|
||||||
|
error: None,
|
||||||
|
bound: None,
|
||||||
|
load_generation: 0,
|
||||||
|
active_tab: 0,
|
||||||
|
pane,
|
||||||
|
commit_scroll_handle: UniformListScrollHandle::new(),
|
||||||
|
tasks: Vec::new(),
|
||||||
|
directory: ProfileDirectory::new(cx),
|
||||||
|
_subscription: subscription,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn track_participants(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let store = self.store.read(cx);
|
||||||
|
let mut keys: Vec<PublicKey> = store
|
||||||
|
.pull_requests
|
||||||
|
.iter()
|
||||||
|
.find(|pr| pr.id == self.pr_id && pr.kind == Kind::GitPullRequest)
|
||||||
|
.map(|root| vec![root.pubkey])
|
||||||
|
.unwrap_or_default();
|
||||||
|
keys.extend(store.comments_of(&self.pr_id).map(|comment| comment.pubkey));
|
||||||
|
if let Some(owner) = store.announcement.as_ref().map(|a| a.owner) {
|
||||||
|
keys.push(owner);
|
||||||
|
}
|
||||||
|
self.directory
|
||||||
|
.update(cx, |directory, cx| directory.track(keys, cx));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.track_participants(cx);
|
||||||
|
let loaded = self.store.read(cx).loaded;
|
||||||
|
let binding = {
|
||||||
|
let store = self.store.read(cx);
|
||||||
|
store.addr().and_then(|addr| {
|
||||||
|
store
|
||||||
|
.pull_requests
|
||||||
|
.iter()
|
||||||
|
.find(|pr| pr.id == self.pr_id && pr.kind == Kind::GitPullRequest)
|
||||||
|
.map(|root| {
|
||||||
|
let update = PullRequest::latest_update(store.pull_requests.iter(), root);
|
||||||
|
let tip = update
|
||||||
|
.and_then(GitEvent::current_commit)
|
||||||
|
.or_else(|| root.current_commit());
|
||||||
|
let base = update
|
||||||
|
.and_then(GitEvent::merge_base)
|
||||||
|
.or_else(|| root.merge_base());
|
||||||
|
let clone_urls = root
|
||||||
|
.clone_urls()
|
||||||
|
.or_else(|| store.announcement.as_ref().map(|a| a.clone.clone()))
|
||||||
|
.unwrap_or_default();
|
||||||
|
PrBinding {
|
||||||
|
description: root.content.clone(),
|
||||||
|
patch: PullRequest::new(root).patch(store.patches.iter()),
|
||||||
|
tip,
|
||||||
|
base,
|
||||||
|
clone_urls,
|
||||||
|
addr: addr.clone(),
|
||||||
|
has_patch_link: root.tags.event_ids().next().is_some(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(binding) = binding else {
|
||||||
|
self.bound = None;
|
||||||
|
if !loaded {
|
||||||
|
self.loading = true;
|
||||||
|
self.error = None;
|
||||||
|
} else if self.error.as_deref() != Some(NOT_FOUND) {
|
||||||
|
self.loading = false;
|
||||||
|
self.error = Some(NOT_FOUND.into());
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.bound.as_ref() == Some(&binding) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.bound = Some(binding.clone());
|
||||||
|
self.load_diff(binding, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_diff(&mut self, binding: PrBinding, cx: &mut Context<Self>) {
|
||||||
|
self.loading = true;
|
||||||
|
self.error = None;
|
||||||
|
self.description = binding.description.clone().into();
|
||||||
|
self.current_commit = binding.tip.clone().map(SharedString::from);
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
self.load_generation = self.load_generation.wrapping_add(1);
|
||||||
|
let generation = self.load_generation;
|
||||||
|
let PrBinding {
|
||||||
|
patch,
|
||||||
|
tip,
|
||||||
|
base,
|
||||||
|
clone_urls,
|
||||||
|
addr,
|
||||||
|
has_patch_link,
|
||||||
|
..
|
||||||
|
} = binding;
|
||||||
|
|
||||||
|
let task: gpui::Task<Result<(), anyhow::Error>> = cx.spawn(async move |this, cx| {
|
||||||
|
let nostr_diff = cx
|
||||||
|
.background_spawn({
|
||||||
|
let patch = patch.clone();
|
||||||
|
async move { PatchParser::patch_diffs(&patch) }
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let nostr_commits = cx
|
||||||
|
.background_spawn({
|
||||||
|
let patch = patch.clone();
|
||||||
|
async move { PatchParser::patch_commits(&patch) }
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let use_nostr = match &nostr_diff {
|
||||||
|
Ok(diff) => has_patch_link || !diff.files.is_empty(),
|
||||||
|
Err(_) => true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let git = if use_nostr {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let addr = addr.clone();
|
||||||
|
let clone_urls = clone_urls.clone();
|
||||||
|
let base = base.clone();
|
||||||
|
let tip = tip.clone();
|
||||||
|
Some(
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let repo = Mirrors::ensure(&addr, &clone_urls)?;
|
||||||
|
let gix_repo = repo.inner();
|
||||||
|
let workdir = gix_repo
|
||||||
|
.workdir()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("repository has no worktree"))?
|
||||||
|
.to_path_buf();
|
||||||
|
let tip =
|
||||||
|
tip.ok_or_else(|| anyhow::anyhow!("pull request has no tip commit"))?;
|
||||||
|
let base = match base {
|
||||||
|
Some(base) => base,
|
||||||
|
None => {
|
||||||
|
let head = gix_repo
|
||||||
|
.head_id()
|
||||||
|
.map_err(|_| anyhow::anyhow!("repository has no HEAD"))?;
|
||||||
|
let tip_id = gix_repo.rev_parse_single(tip.as_bytes())?;
|
||||||
|
gix_repo.merge_base(tip_id, head)?.to_string()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let diff = repo.range_diff(&base, &tip)?;
|
||||||
|
let commits = repo.commit_range(&base, &tip)?;
|
||||||
|
Ok::<_, anyhow::Error>((diff, commits, workdir))
|
||||||
|
})
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let (diff, commits, worktree) = match git {
|
||||||
|
Some(Ok((diff, commits, worktree))) => (Ok(diff), commits, Some(worktree)),
|
||||||
|
Some(Err(error)) => (Err(error), Vec::new(), None),
|
||||||
|
None => (nostr_diff, nostr_commits, None),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
if this.load_generation != generation {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
this.worktree = worktree;
|
||||||
|
this.commits = commits;
|
||||||
|
match diff {
|
||||||
|
Ok(diff) => this.pane.update(cx, |pane, cx| pane.set_diff(diff, cx)),
|
||||||
|
Err(error) => this.error = Some(error.to_string().into()),
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
self.tasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_commit_diff(
|
||||||
|
&mut self,
|
||||||
|
worktree: PathBuf,
|
||||||
|
commit_id: &str,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let id = match self.store.read(cx).addr() {
|
||||||
|
Some(addr) => SharedString::from(format!("commit:{addr}:{commit_id}")),
|
||||||
|
None => SharedString::from(format!("commit:{}:{commit_id}", worktree.display())),
|
||||||
|
};
|
||||||
|
let dock = self.dock.clone();
|
||||||
|
let dock_for_build = dock.clone();
|
||||||
|
let store = self.store.clone();
|
||||||
|
let repo_name = self.repo_name.clone();
|
||||||
|
let commit_id = commit_id.to_owned();
|
||||||
|
crate::shared::panels::open(
|
||||||
|
&dock,
|
||||||
|
id,
|
||||||
|
move |window, cx| {
|
||||||
|
cx.new(|cx| {
|
||||||
|
CommitDiffView::new(
|
||||||
|
store.clone(),
|
||||||
|
worktree.clone(),
|
||||||
|
repo_name.clone(),
|
||||||
|
commit_id.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
let _ = dock_for_build;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_tabs(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
|
let files = self.pane.read(cx).diff().map(|diff| diff.files.len());
|
||||||
|
let commits = (!self.commits.is_empty()).then_some(self.commits.len());
|
||||||
|
let weak = cx.entity().downgrade();
|
||||||
|
let files_label = match files {
|
||||||
|
Some(count) => format!("Files ({count})"),
|
||||||
|
None => "Files".to_owned(),
|
||||||
|
};
|
||||||
|
let commits_label = match commits {
|
||||||
|
Some(count) => format!("Commits ({count})"),
|
||||||
|
None => "Commits".to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Tabs::new("pr-tabs")
|
||||||
|
.selected(self.active_tab)
|
||||||
|
.tabs(move |_, _| {
|
||||||
|
vec![
|
||||||
|
Tab::new("discussion", "Discussion"),
|
||||||
|
Tab::new("files", files_label.clone()),
|
||||||
|
Tab::new("commits", commits_label.clone()),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.on_change(move |index, _, cx| {
|
||||||
|
let _ = weak.update(cx, |this, cx| {
|
||||||
|
this.active_tab = index;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_discussion(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
|
if self.loading {
|
||||||
|
return centered_spinner();
|
||||||
|
}
|
||||||
|
if let Some(error) = self.error.clone() {
|
||||||
|
return empty_state(error);
|
||||||
|
}
|
||||||
|
let (author, picture, age, root_id, pubkey) = {
|
||||||
|
let store = self.store.read(cx);
|
||||||
|
let Some(root) = store
|
||||||
|
.pull_requests
|
||||||
|
.iter()
|
||||||
|
.find(|pr| pr.id == self.pr_id && pr.kind == Kind::GitPullRequest)
|
||||||
|
else {
|
||||||
|
return empty_state(NOT_FOUND);
|
||||||
|
};
|
||||||
|
let profile = self.directory.read(cx).profile(&root.pubkey, cx);
|
||||||
|
(
|
||||||
|
profile.name(),
|
||||||
|
profile.picture(),
|
||||||
|
relative_time(root.created_at),
|
||||||
|
root.id,
|
||||||
|
root.pubkey,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.size_full()
|
||||||
|
.min_h_0()
|
||||||
|
.child(
|
||||||
|
ScrollArea::new("pr-discussion-scroll")
|
||||||
|
.flex_1()
|
||||||
|
.min_w_0()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.p_4()
|
||||||
|
.gap_6()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_4()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.text_sm()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
Avatar::new(pubkey)
|
||||||
|
.name(author.clone())
|
||||||
|
.picture(picture)
|
||||||
|
.small(),
|
||||||
|
)
|
||||||
|
.child(author),
|
||||||
|
)
|
||||||
|
.child(SharedString::from("commented"))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(cx.theme().colors.muted_foreground)
|
||||||
|
.child(SharedString::from(age)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.when(!self.description.is_empty(), |this| {
|
||||||
|
this.child(div().text_sm().child(self.description.clone()))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(comments_section(&self.store, root_id, &self.directory, cx))
|
||||||
|
.child(comment_form(
|
||||||
|
&self.store,
|
||||||
|
root_id,
|
||||||
|
pr_roots,
|
||||||
|
&self.comment_input,
|
||||||
|
"pr-comment",
|
||||||
|
cx,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(sidebar_section(
|
||||||
|
&self.store,
|
||||||
|
root_id,
|
||||||
|
pr_roots,
|
||||||
|
true,
|
||||||
|
&self.directory,
|
||||||
|
cx,
|
||||||
|
))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_files(&self, _cx: &mut Context<Self>) -> AnyElement {
|
||||||
|
if self.loading {
|
||||||
|
return centered_spinner();
|
||||||
|
}
|
||||||
|
if let Some(error) = self.error.clone() {
|
||||||
|
return empty_state(error);
|
||||||
|
}
|
||||||
|
self.pane.clone().into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_commits(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
|
if self.loading {
|
||||||
|
return centered_spinner();
|
||||||
|
}
|
||||||
|
if let Some(error) = self.error.clone() {
|
||||||
|
return empty_state(error);
|
||||||
|
}
|
||||||
|
if self.commits.is_empty() {
|
||||||
|
return empty_state("No commits found");
|
||||||
|
}
|
||||||
|
let commits = self.commits.clone();
|
||||||
|
let worktree = self.worktree.clone();
|
||||||
|
let view = cx.entity().downgrade();
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.relative()
|
||||||
|
.flex_1()
|
||||||
|
.min_h_0()
|
||||||
|
.child(
|
||||||
|
uniform_list("pr-commits", commits.len(), move |range, _, cx| {
|
||||||
|
range
|
||||||
|
.map(|ix| {
|
||||||
|
let commit = commits[ix].clone();
|
||||||
|
let id = commit.id.clone();
|
||||||
|
let worktree = worktree.clone();
|
||||||
|
let view = view.clone();
|
||||||
|
commit_row(
|
||||||
|
ix,
|
||||||
|
&commit,
|
||||||
|
move |window, cx| {
|
||||||
|
if let Some(worktree) = worktree.clone() {
|
||||||
|
let _ = view.update(cx, |this, cx| {
|
||||||
|
this.open_commit_diff(worktree, &id, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.track_scroll(&self.commit_scroll_handle)
|
||||||
|
.size_full(),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.top_0()
|
||||||
|
.left_0()
|
||||||
|
.right_0()
|
||||||
|
.bottom_0()
|
||||||
|
.child(Scrollbar::vertical(
|
||||||
|
"pr-commits-scroll",
|
||||||
|
self.commit_scroll_handle.clone(),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
|
let current_commit = self.current_commit.clone();
|
||||||
|
let (title, status, branch, author) = {
|
||||||
|
let store = self.store.read(cx);
|
||||||
|
let Some(root) = store
|
||||||
|
.pull_requests
|
||||||
|
.iter()
|
||||||
|
.find(|pr| pr.id == self.pr_id && pr.kind == Kind::GitPullRequest)
|
||||||
|
else {
|
||||||
|
return div().into_any_element();
|
||||||
|
};
|
||||||
|
(
|
||||||
|
root.activity_subject(),
|
||||||
|
store.status_of(root),
|
||||||
|
root.branch_name(),
|
||||||
|
root.pubkey,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let can_update = Backend::global(cx).read(cx).current_user() == Some(author);
|
||||||
|
let weak = cx.entity().downgrade();
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.flex_none()
|
||||||
|
.px_4()
|
||||||
|
.py_2()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
h_flex().gap_2().child(status_badge(status)).child(
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.min_w_0()
|
||||||
|
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||||
|
.text_ellipsis()
|
||||||
|
.child(title),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_3()
|
||||||
|
.when_some(branch, |this, branch| {
|
||||||
|
this.child(
|
||||||
|
Button::new("branch")
|
||||||
|
.label(branch)
|
||||||
|
.icon(Icon::named(CustomIconName::GitBranch))
|
||||||
|
.small(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when(can_update, |this| {
|
||||||
|
this.child(Button::new("update-pr").label("Update").small().on_click(
|
||||||
|
move |_, _, cx| {
|
||||||
|
let _ = weak.update(cx, |this, cx| {
|
||||||
|
let root = this
|
||||||
|
.store
|
||||||
|
.read(cx)
|
||||||
|
.pull_requests
|
||||||
|
.iter()
|
||||||
|
.find(|pr| {
|
||||||
|
pr.id == this.pr_id && pr.kind == Kind::GitPullRequest
|
||||||
|
})
|
||||||
|
.cloned();
|
||||||
|
if let Some(root) = root {
|
||||||
|
dialogs::update::open(this.store.clone(), root, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.when_some(current_commit, |this, id| {
|
||||||
|
this.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.font_family(cx.theme().mono_font)
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().colors.muted_foreground)
|
||||||
|
.child(id.clone())
|
||||||
|
.child(ClipboardButton::new("pr-commit", &id)),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Panel for PullRequestDetailView {
|
||||||
|
fn title(&self, _cx: &App) -> Option<SharedString> {
|
||||||
|
let hex = self.pr_id.to_hex();
|
||||||
|
Some(super::helpers::pr_label(&self.repo_name, &hex[..8]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lead(&self, cx: &App) -> Option<AnyElement> {
|
||||||
|
Some(repo_tab_avatar(self.store.read(cx), &self.directory, cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for PullRequestDetailView {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.id("pull-request-detail")
|
||||||
|
.size_full()
|
||||||
|
.min_h_0()
|
||||||
|
.child(self.render_header(cx))
|
||||||
|
.child(self.render_tabs(cx))
|
||||||
|
.map(|this| match self.active_tab {
|
||||||
|
0 => this.child(self.render_discussion(cx)),
|
||||||
|
1 => this.child(self.render_files(cx)),
|
||||||
|
_ => this.child(self.render_commits(cx)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn centered_spinner() -> AnyElement {
|
||||||
|
v_flex()
|
||||||
|
.size_full()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.child(Spinner::new("pr-loading"))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod update;
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
use gpui::{
|
||||||
|
AnyElement, App, AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render,
|
||||||
|
Styled as _, Window, div,
|
||||||
|
};
|
||||||
|
use gpui_native_kit::button::Button;
|
||||||
|
use gpui_native_kit::common::v_flex;
|
||||||
|
use gpui_native_kit::dialog::Dialog;
|
||||||
|
use gpui_native_kit::field::Field;
|
||||||
|
use gpui_native_kit::input::{Input, InputState};
|
||||||
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
|
use nostr::prelude::Event;
|
||||||
|
use signed_state::RepoStore;
|
||||||
|
|
||||||
|
use crate::shared::dialogs::{close_global, open_global};
|
||||||
|
|
||||||
|
pub fn open(store: Entity<RepoStore>, root: Event, cx: &mut App) {
|
||||||
|
open_global(UpdatePrDialog::new(store, root, cx), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UpdatePrDialog {
|
||||||
|
store: Entity<RepoStore>,
|
||||||
|
root: Event,
|
||||||
|
patch: Entity<InputState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdatePrDialog {
|
||||||
|
pub fn new(store: Entity<RepoStore>, root: Event, cx: &mut App) -> Entity<Self> {
|
||||||
|
let patch = InputState::new(cx);
|
||||||
|
patch.update(cx, |state, cx| state.set_multiline(true, cx));
|
||||||
|
cx.new(|_| Self { store, root, patch })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn submit(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let patch = self.patch.read(cx).value().to_string();
|
||||||
|
if patch.trim().is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.store.update(cx, |store, cx| {
|
||||||
|
store.update_pull_request(&self.root, patch, cx);
|
||||||
|
});
|
||||||
|
close_global(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for UpdatePrDialog {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let patch = self.patch.clone();
|
||||||
|
let this = cx.entity().downgrade();
|
||||||
|
|
||||||
|
Dialog::new("update-pr-dialog")
|
||||||
|
.open(true)
|
||||||
|
.min_width(gpui::px(520.))
|
||||||
|
.on_change(|open, _, cx| {
|
||||||
|
if !open {
|
||||||
|
close_global(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.title("Update pull request")
|
||||||
|
.content(move |_, cx| {
|
||||||
|
v_flex()
|
||||||
|
.gap_3()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().colors.muted_foreground)
|
||||||
|
.child("Publish a new revision with the output of `git format-patch`."),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Field::new("update-patch", "Patch")
|
||||||
|
.child(Input::new("update-patch-input", patch.clone()).rows(6)),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
})
|
||||||
|
.actions(move |_, _| {
|
||||||
|
let this = this.clone();
|
||||||
|
vec![
|
||||||
|
Button::new("submit-update")
|
||||||
|
.label("Update pull request")
|
||||||
|
.on_click(move |_, _, cx| {
|
||||||
|
let _ = this.update(cx, |this, cx| this.submit(cx));
|
||||||
|
})
|
||||||
|
.into_any_element() as AnyElement,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
use gpui::SharedString;
|
||||||
|
use signed_core::RepoStatus;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum PullRequestFilter {
|
||||||
|
All,
|
||||||
|
Open,
|
||||||
|
Closed,
|
||||||
|
Draft,
|
||||||
|
Merged,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PullRequestFilter {
|
||||||
|
pub fn matches(self, status: RepoStatus) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::All => true,
|
||||||
|
Self::Open => status == RepoStatus::Open,
|
||||||
|
Self::Closed => status == RepoStatus::Closed,
|
||||||
|
Self::Draft => status == RepoStatus::Draft,
|
||||||
|
Self::Merged => status == RepoStatus::Applied,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn empty_message(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::All => "No pull requests",
|
||||||
|
Self::Open => "No open pull requests",
|
||||||
|
Self::Closed => "No closed pull requests",
|
||||||
|
Self::Draft => "No draft pull requests",
|
||||||
|
Self::Merged => "No merged pull requests",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pr_label(repo_name: &SharedString, pr_id: &str) -> SharedString {
|
||||||
|
if repo_name.is_empty() {
|
||||||
|
SharedString::from(pr_id)
|
||||||
|
} else {
|
||||||
|
SharedString::from(format!("{repo_name}/{pr_id}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use nostr::prelude::PublicKey;
|
||||||
|
use signed_core::Announcement;
|
||||||
|
use utils::middle_truncate;
|
||||||
|
|
||||||
|
pub struct ForkCompare {
|
||||||
|
pub announcement: Announcement,
|
||||||
|
pub namespace: String,
|
||||||
|
pub mirror_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ForkCompare {
|
||||||
|
pub fn base_ref(name: &str) -> String {
|
||||||
|
format!("refs/remotes/origin/{name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compare_ref(&self, name: &str) -> String {
|
||||||
|
format!("refs/fork/{}/{}", self.namespace, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fork_display_name(announcement: &Announcement) -> SharedString {
|
||||||
|
announcement
|
||||||
|
.name
|
||||||
|
.as_deref()
|
||||||
|
.map(SharedString::from)
|
||||||
|
.unwrap_or_else(|| SharedString::from(announcement.id.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shorten_owner(owner: &PublicKey) -> String {
|
||||||
|
owner.to_hex().chars().take(10).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn truncate_label(label: &str) -> SharedString {
|
||||||
|
SharedString::from(middle_truncate(label, 18, 0))
|
||||||
|
}
|
||||||
@@ -0,0 +1,455 @@
|
|||||||
|
use assets::CustomIconName;
|
||||||
|
use gpui::prelude::FluentBuilder as _;
|
||||||
|
use gpui::{
|
||||||
|
AnyElement, App, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement,
|
||||||
|
ParentElement as _, Render, SharedString, StatefulInteractiveElement as _, Styled as _,
|
||||||
|
Subscription, UniformListScrollHandle, WeakEntity, Window, div, px, uniform_list,
|
||||||
|
};
|
||||||
|
use gpui_native_kit::alert::{Alert, AlertSeverity};
|
||||||
|
use gpui_native_kit::avatar::Avatar;
|
||||||
|
use gpui_native_kit::common::{Sizable as _, h_flex, v_flex};
|
||||||
|
use gpui_native_kit::dropdown_menu::DropdownMenu;
|
||||||
|
use gpui_native_kit::icons::Icon;
|
||||||
|
use gpui_native_kit::layout::dock::{DockState, Panel};
|
||||||
|
use gpui_native_kit::menu::MenuItem;
|
||||||
|
use gpui_native_kit::scrollbar::Scrollbar;
|
||||||
|
use gpui_native_kit::segmented::{Segmented, SegmentedOption};
|
||||||
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
|
use metadata::ProfileDirectory;
|
||||||
|
use nostr::prelude::{Kind, PublicKey};
|
||||||
|
use signed_core::GitEvent;
|
||||||
|
use signed_state::RepoStore;
|
||||||
|
use utils::relative_time;
|
||||||
|
|
||||||
|
mod detail;
|
||||||
|
mod dialogs;
|
||||||
|
mod helpers;
|
||||||
|
mod new;
|
||||||
|
mod send_patch;
|
||||||
|
|
||||||
|
pub use detail::PullRequestDetailView;
|
||||||
|
use helpers::PullRequestFilter;
|
||||||
|
pub use new::NewPullRequestView;
|
||||||
|
pub use send_patch::SendPatchView;
|
||||||
|
|
||||||
|
use crate::issues::helpers::{StatusCounts, filter_by_status};
|
||||||
|
use crate::shared::widgets::{empty_state, repo_tab_avatar, status_badge};
|
||||||
|
|
||||||
|
const ROW_HEIGHT: f32 = 73.;
|
||||||
|
|
||||||
|
pub struct PullRequestsView {
|
||||||
|
dock: WeakEntity<DockState>,
|
||||||
|
store: Entity<RepoStore>,
|
||||||
|
repo_name: SharedString,
|
||||||
|
filter: PullRequestFilter,
|
||||||
|
visible_prs: Vec<usize>,
|
||||||
|
counts: StatusCounts,
|
||||||
|
synced_filter: PullRequestFilter,
|
||||||
|
scroll_handle: UniformListScrollHandle,
|
||||||
|
directory: Entity<ProfileDirectory>,
|
||||||
|
new_pr_menu_open: bool,
|
||||||
|
_subscription: Subscription,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PullRequestsView {
|
||||||
|
pub fn new(
|
||||||
|
dock: WeakEntity<DockState>,
|
||||||
|
store: Entity<RepoStore>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let weak = cx.entity().downgrade();
|
||||||
|
let repo_name = store.read(cx).name();
|
||||||
|
let subscription = cx.observe(&store, |this, _store, cx| {
|
||||||
|
this.rebuild(cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.defer(move |cx| {
|
||||||
|
let _ = weak.update(cx, |this, cx| {
|
||||||
|
this.rebuild(cx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
dock,
|
||||||
|
store,
|
||||||
|
repo_name,
|
||||||
|
filter: PullRequestFilter::Open,
|
||||||
|
visible_prs: Vec::new(),
|
||||||
|
counts: StatusCounts::default(),
|
||||||
|
synced_filter: PullRequestFilter::Open,
|
||||||
|
scroll_handle: UniformListScrollHandle::new(),
|
||||||
|
directory: ProfileDirectory::new(cx),
|
||||||
|
new_pr_menu_open: false,
|
||||||
|
_subscription: subscription,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let store = self.store.read(cx);
|
||||||
|
let filter = self.filter;
|
||||||
|
|
||||||
|
let (visible_prs, counts) = {
|
||||||
|
let roots = store
|
||||||
|
.pull_requests
|
||||||
|
.iter()
|
||||||
|
.filter(|pr| pr.kind == Kind::GitPullRequest);
|
||||||
|
|
||||||
|
filter_by_status(
|
||||||
|
roots,
|
||||||
|
|pr| store.status_of(pr),
|
||||||
|
|status| filter.matches(status),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut keys: Vec<PublicKey> = store
|
||||||
|
.pull_requests
|
||||||
|
.iter()
|
||||||
|
.filter(|pr| pr.kind == Kind::GitPullRequest)
|
||||||
|
.map(|pr| pr.pubkey)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if let Some(owner) = store
|
||||||
|
.announcement
|
||||||
|
.as_ref()
|
||||||
|
.map(|announcement| announcement.owner)
|
||||||
|
{
|
||||||
|
keys.push(owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.directory.update(cx, |directory, cx| {
|
||||||
|
directory.track(keys, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
let filter_changed = self.synced_filter != filter;
|
||||||
|
if !filter_changed && self.visible_prs == visible_prs && self.counts == counts {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.synced_filter = filter;
|
||||||
|
self.visible_prs = visible_prs;
|
||||||
|
self.counts = counts;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_new(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(addr) = self.store.read(cx).addr().cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let id = SharedString::from(format!("new-pr:{addr}"));
|
||||||
|
let dock = self.dock.clone();
|
||||||
|
let dock_for_build = dock.clone();
|
||||||
|
let store = self.store.clone();
|
||||||
|
|
||||||
|
crate::shared::panels::open(
|
||||||
|
&dock,
|
||||||
|
id.clone(),
|
||||||
|
move |window, cx| {
|
||||||
|
cx.new(|cx| {
|
||||||
|
NewPullRequestView::new(
|
||||||
|
dock_for_build.clone(),
|
||||||
|
id.clone(),
|
||||||
|
store.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_send_patch(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(addr) = self.store.read(cx).addr().cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let id = SharedString::from(format!("send-patch:{addr}"));
|
||||||
|
let dock = self.dock.clone();
|
||||||
|
let dock_for_build = dock.clone();
|
||||||
|
let store = self.store.clone();
|
||||||
|
|
||||||
|
crate::shared::panels::open(
|
||||||
|
&dock,
|
||||||
|
id.clone(),
|
||||||
|
move |window, cx| {
|
||||||
|
cx.new(|cx| {
|
||||||
|
SendPatchView::new(
|
||||||
|
dock_for_build.clone(),
|
||||||
|
id.clone(),
|
||||||
|
store.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
|
let counts = self.counts;
|
||||||
|
let muted = cx.theme().colors.muted_foreground;
|
||||||
|
let weak = cx.entity().downgrade();
|
||||||
|
let open_weak = cx.entity().downgrade();
|
||||||
|
let menu_open_weak = cx.entity().downgrade();
|
||||||
|
let patch_weak = cx.entity().downgrade();
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.px_4()
|
||||||
|
.w_full()
|
||||||
|
.gap_3()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(cx.theme().colors.border)
|
||||||
|
.bg(cx.theme().colors.muted)
|
||||||
|
.child(
|
||||||
|
Segmented::new("pr-filter", self.filter)
|
||||||
|
.options(vec![
|
||||||
|
SegmentedOption::new(
|
||||||
|
PullRequestFilter::All,
|
||||||
|
format!("All ({})", counts.total),
|
||||||
|
)
|
||||||
|
.icon(Icon::named(CustomIconName::GitPullRequest).color(muted)),
|
||||||
|
SegmentedOption::new(
|
||||||
|
PullRequestFilter::Open,
|
||||||
|
format!("Open ({})", counts.open),
|
||||||
|
)
|
||||||
|
.icon(Icon::named(CustomIconName::GitPullRequest).color(muted)),
|
||||||
|
SegmentedOption::new(
|
||||||
|
PullRequestFilter::Closed,
|
||||||
|
format!("Closed ({})", counts.closed),
|
||||||
|
)
|
||||||
|
.icon(Icon::named(CustomIconName::GitPullRequestClosed).color(muted)),
|
||||||
|
SegmentedOption::new(
|
||||||
|
PullRequestFilter::Draft,
|
||||||
|
format!("Draft ({})", counts.draft),
|
||||||
|
)
|
||||||
|
.icon(Icon::named(CustomIconName::GitPullRequestDraft).color(muted)),
|
||||||
|
SegmentedOption::new(
|
||||||
|
PullRequestFilter::Merged,
|
||||||
|
format!("Merged ({})", counts.applied),
|
||||||
|
)
|
||||||
|
.icon(Icon::named(CustomIconName::GitPullRequestMerged).color(muted)),
|
||||||
|
])
|
||||||
|
.on_change(move |filter, _, cx| {
|
||||||
|
if let Err(error) = weak.update(cx, |this, cx| {
|
||||||
|
this.filter = filter;
|
||||||
|
this.rebuild(cx);
|
||||||
|
}) {
|
||||||
|
log::warn!("failed to switch PR filter: {error}");
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(div().flex_1())
|
||||||
|
.child(
|
||||||
|
DropdownMenu::new("new-pr-actions")
|
||||||
|
.label("New")
|
||||||
|
.icon(Icon::named(CustomIconName::GitPullRequest))
|
||||||
|
.open(self.new_pr_menu_open)
|
||||||
|
.on_click(move |_, window, cx| {
|
||||||
|
if let Err(error) =
|
||||||
|
open_weak.update(cx, |this, cx| this.open_new(window, cx))
|
||||||
|
{
|
||||||
|
log::warn!("failed to open new PR: {error}");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_open_change(move |open, _, cx| {
|
||||||
|
let _ = menu_open_weak.update(cx, |this, cx| {
|
||||||
|
this.new_pr_menu_open = open;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.items(move |_, _| {
|
||||||
|
vec![MenuItem::new("Send Patch").handler({
|
||||||
|
let weak = patch_weak.clone();
|
||||||
|
move |window, cx| {
|
||||||
|
let _ = weak.update(cx, |this, cx| {
|
||||||
|
this.open_send_patch(window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Panel for PullRequestsView {
|
||||||
|
fn title(&self, _cx: &App) -> Option<SharedString> {
|
||||||
|
Some(SharedString::from(format!("{}/pulls", self.repo_name)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lead(&self, cx: &App) -> Option<AnyElement> {
|
||||||
|
Some(repo_tab_avatar(self.store.read(cx), &self.directory, cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for PullRequestsView {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let filter = self.filter;
|
||||||
|
let count = self.visible_prs.len();
|
||||||
|
let visible = self.visible_prs.clone();
|
||||||
|
let directory = self.directory.clone();
|
||||||
|
let store = self.store.clone();
|
||||||
|
let dock = self.dock.clone();
|
||||||
|
|
||||||
|
let (last_error, last_warning) = {
|
||||||
|
let store = self.store.read(cx);
|
||||||
|
(store.last_error.clone(), store.last_warning.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
let dismiss_warning_store = self.store.clone();
|
||||||
|
let dismiss_error_store = self.store.clone();
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.size_full()
|
||||||
|
.child(self.render_header(cx))
|
||||||
|
.when_some(last_warning, |this, warning| {
|
||||||
|
this.child(
|
||||||
|
Alert::new("pr-warning")
|
||||||
|
.severity(AlertSeverity::Warning)
|
||||||
|
.title("Pull request warning")
|
||||||
|
.message(warning)
|
||||||
|
.on_dismiss(move |cx| {
|
||||||
|
dismiss_warning_store.update(cx, |store, _| store.last_warning = None);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when_some(last_error, |this, error| {
|
||||||
|
this.child(
|
||||||
|
Alert::new("pr-error")
|
||||||
|
.severity(AlertSeverity::Danger)
|
||||||
|
.title("Pull request error")
|
||||||
|
.message(error)
|
||||||
|
.on_dismiss(move |cx| {
|
||||||
|
dismiss_error_store.update(cx, |store, _| store.last_error = None);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.relative()
|
||||||
|
.flex_1()
|
||||||
|
.min_h_0()
|
||||||
|
.w_full()
|
||||||
|
.when(count > 0, |this| {
|
||||||
|
this.child(
|
||||||
|
uniform_list("prl", count, move |range, _, cx| {
|
||||||
|
range
|
||||||
|
.map(|ix| {
|
||||||
|
let pr_ix = visible[ix];
|
||||||
|
let pr = &store.read(cx).pull_requests[pr_ix];
|
||||||
|
let pr_id = pr.id;
|
||||||
|
let title = pr.activity_subject();
|
||||||
|
let id_hex = pr.id.to_hex();
|
||||||
|
let age = relative_time(pr.created_at);
|
||||||
|
let status = store.read(cx).status_of(pr);
|
||||||
|
let profile = directory.read(cx).profile(&pr.pubkey, cx);
|
||||||
|
let author = profile.name();
|
||||||
|
let dock = dock.clone();
|
||||||
|
let store = store.clone();
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.id(ix)
|
||||||
|
.w_full()
|
||||||
|
.h(px(ROW_HEIGHT))
|
||||||
|
.gap_4()
|
||||||
|
.px_4()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(cx.theme().colors.border)
|
||||||
|
.hover(|this| this.bg(cx.theme().colors.muted_hover))
|
||||||
|
.on_click(move |_, window, cx| {
|
||||||
|
let id = SharedString::from(format!("pr:{pr_id}"));
|
||||||
|
let dock_for_build = dock.clone();
|
||||||
|
let store = store.clone();
|
||||||
|
crate::shared::panels::open(
|
||||||
|
&dock,
|
||||||
|
id,
|
||||||
|
move |window, cx| {
|
||||||
|
cx.new(|cx| {
|
||||||
|
PullRequestDetailView::new(
|
||||||
|
dock_for_build.clone(),
|
||||||
|
store.clone(),
|
||||||
|
pr_id,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.child(status_badge(status))
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.flex_1()
|
||||||
|
.min_w_0()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.text_ellipsis()
|
||||||
|
.whitespace_nowrap()
|
||||||
|
.child(title),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.text_xs()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
Avatar::new(pr.pubkey)
|
||||||
|
.name(author.clone())
|
||||||
|
.picture(
|
||||||
|
profile.picture(),
|
||||||
|
)
|
||||||
|
.small(),
|
||||||
|
)
|
||||||
|
.child(div().child(author)),
|
||||||
|
)
|
||||||
|
.child(SharedString::from("opened"))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(
|
||||||
|
cx.theme()
|
||||||
|
.colors
|
||||||
|
.muted_foreground,
|
||||||
|
)
|
||||||
|
.child(SharedString::from(
|
||||||
|
&id_hex[..8],
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.child(SharedString::from(age)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.track_scroll(&self.scroll_handle)
|
||||||
|
.size_full(),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.top_0()
|
||||||
|
.left_0()
|
||||||
|
.right_0()
|
||||||
|
.bottom_0()
|
||||||
|
.child(Scrollbar::vertical(
|
||||||
|
"prl-scroll",
|
||||||
|
self.scroll_handle.clone(),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when(count == 0, |this| {
|
||||||
|
this.child(empty_state(filter.empty_message()))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,922 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use assets::CustomIconName;
|
||||||
|
use gpui::prelude::FluentBuilder as _;
|
||||||
|
use gpui::{
|
||||||
|
AnyElement, App, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement,
|
||||||
|
ParentElement as _, PathPromptOptions, Render, SharedString, Styled as _, Subscription,
|
||||||
|
UniformListScrollHandle, WeakEntity, Window, div, px, uniform_list,
|
||||||
|
};
|
||||||
|
use gpui_native_kit::button::Button;
|
||||||
|
use gpui_native_kit::combobox::{Combobox, ComboboxState};
|
||||||
|
use gpui_native_kit::common::{h_flex, v_flex};
|
||||||
|
use gpui_native_kit::data::tabs::{Tab, Tabs};
|
||||||
|
use gpui_native_kit::icons::{Icon, IconRole};
|
||||||
|
use gpui_native_kit::input::{Input, InputState};
|
||||||
|
use gpui_native_kit::layout::dock::{DockState, Panel};
|
||||||
|
use gpui_native_kit::menu::{Menu, MenuItem};
|
||||||
|
use gpui_native_kit::scrollbar::Scrollbar;
|
||||||
|
use gpui_native_kit::spinner::Spinner;
|
||||||
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
|
use metadata::ProfileDirectory;
|
||||||
|
use nostr::prelude::Url;
|
||||||
|
use signed_core::Announcement;
|
||||||
|
use signed_git::{GitCache, Repo};
|
||||||
|
use signed_state::{Backend, CheckoutsStore, Mirrors, RepoListStore, RepoStore};
|
||||||
|
|
||||||
|
use super::helpers::{ForkCompare, fork_display_name, shorten_owner, truncate_label};
|
||||||
|
use crate::repo::commit_diff::{CommitDiffView, DiffPane};
|
||||||
|
use crate::repo::helpers::commit_row;
|
||||||
|
use crate::shared::widgets::empty_state;
|
||||||
|
|
||||||
|
pub struct NewPullRequestView {
|
||||||
|
dock: WeakEntity<DockState>,
|
||||||
|
panel_id: SharedString,
|
||||||
|
store: Entity<RepoStore>,
|
||||||
|
repo_name: SharedString,
|
||||||
|
repo_path: Option<PathBuf>,
|
||||||
|
branches: Vec<SharedString>,
|
||||||
|
fork: Option<ForkCompare>,
|
||||||
|
base: SharedString,
|
||||||
|
compare: SharedString,
|
||||||
|
base_select: Entity<ComboboxState<SharedString>>,
|
||||||
|
compare_select: Entity<ComboboxState<SharedString>>,
|
||||||
|
subject: Entity<InputState>,
|
||||||
|
description: Entity<InputState>,
|
||||||
|
merge_base: Option<String>,
|
||||||
|
commits: Option<Vec<signed_git::FileCommit>>,
|
||||||
|
loading: bool,
|
||||||
|
error: Option<SharedString>,
|
||||||
|
submitting: bool,
|
||||||
|
compare_generation: u64,
|
||||||
|
active_tab: usize,
|
||||||
|
pane: Entity<DiffPane>,
|
||||||
|
scroll_handle: UniformListScrollHandle,
|
||||||
|
tasks: Vec<gpui::Task<Result<(), anyhow::Error>>>,
|
||||||
|
directory: Entity<ProfileDirectory>,
|
||||||
|
source_open: bool,
|
||||||
|
_subscriptions: Vec<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NewPullRequestView {
|
||||||
|
pub fn new(
|
||||||
|
dock: WeakEntity<DockState>,
|
||||||
|
panel_id: SharedString,
|
||||||
|
store: Entity<RepoStore>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let directory = ProfileDirectory::new(cx);
|
||||||
|
let repo_name = store.read(cx).name();
|
||||||
|
let pane = cx.new(DiffPane::new);
|
||||||
|
|
||||||
|
let weak = cx.entity().downgrade();
|
||||||
|
let subject = InputState::new(cx);
|
||||||
|
|
||||||
|
let description = InputState::new(cx);
|
||||||
|
description.update(cx, |state, cx| state.set_multiline(true, cx));
|
||||||
|
|
||||||
|
let base_select = cx.new(|cx| {
|
||||||
|
ComboboxState::new(
|
||||||
|
Combobox::new("pr-base", "Base", Vec::new()).on_change({
|
||||||
|
let weak = weak.clone();
|
||||||
|
move |value, _window, cx| {
|
||||||
|
if let Some(base) = value {
|
||||||
|
let _ = weak.update(cx, |this, cx| {
|
||||||
|
this.base = base;
|
||||||
|
this.reload_compare(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let compare_select = cx.new(|cx| {
|
||||||
|
ComboboxState::new(
|
||||||
|
Combobox::new("pr-compare", "Compare", Vec::new()).on_change({
|
||||||
|
let weak = weak.clone();
|
||||||
|
move |value, _window, cx| {
|
||||||
|
if let Some(compare) = value {
|
||||||
|
let _ = weak.update(cx, |this, cx| {
|
||||||
|
this.compare = compare;
|
||||||
|
this.reload_compare(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(owner) = store.read(cx).announcement.as_ref().map(|a| a.owner) {
|
||||||
|
directory.update(cx, |directory, cx| directory.track([owner], cx));
|
||||||
|
}
|
||||||
|
|
||||||
|
window.defer(cx, move |window, cx| {
|
||||||
|
let _ = weak.update(cx, |this, cx| {
|
||||||
|
let Some(addr) = this.store.read(cx).addr().cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(path) = CheckoutsStore::global(cx)
|
||||||
|
.read(cx)
|
||||||
|
.associations_of(&addr)
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.apply_folder_path(path, window, cx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
dock,
|
||||||
|
panel_id,
|
||||||
|
store,
|
||||||
|
repo_name,
|
||||||
|
repo_path: None,
|
||||||
|
branches: Vec::new(),
|
||||||
|
fork: None,
|
||||||
|
base: SharedString::default(),
|
||||||
|
compare: SharedString::default(),
|
||||||
|
base_select,
|
||||||
|
compare_select,
|
||||||
|
subject,
|
||||||
|
description,
|
||||||
|
merge_base: None,
|
||||||
|
commits: None,
|
||||||
|
loading: false,
|
||||||
|
error: None,
|
||||||
|
submitting: false,
|
||||||
|
compare_generation: 0,
|
||||||
|
active_tab: 0,
|
||||||
|
pane,
|
||||||
|
scroll_handle: UniformListScrollHandle::new(),
|
||||||
|
tasks: Vec::new(),
|
||||||
|
directory,
|
||||||
|
source_open: false,
|
||||||
|
_subscriptions: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_source(&self) -> bool {
|
||||||
|
self.repo_path.is_some() || self.fork.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn work_path(&self) -> Option<PathBuf> {
|
||||||
|
match &self.fork {
|
||||||
|
Some(fork) => Some(fork.mirror_path.clone()),
|
||||||
|
None => self.repo_path.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_ref(&self) -> String {
|
||||||
|
match &self.fork {
|
||||||
|
Some(_) => ForkCompare::base_ref(&self.base),
|
||||||
|
None => self.base.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compare_ref(&self) -> String {
|
||||||
|
match &self.fork {
|
||||||
|
Some(fork) => fork.compare_ref(&self.compare),
|
||||||
|
None => self.compare.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn choose_checkout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let prompt = cx.prompt_for_paths(PathPromptOptions {
|
||||||
|
files: false,
|
||||||
|
directories: true,
|
||||||
|
multiple: false,
|
||||||
|
prompt: Some("Choose local checkout".into()),
|
||||||
|
});
|
||||||
|
let task: gpui::Task<Result<(), anyhow::Error>> =
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let picked = match prompt.await {
|
||||||
|
Ok(Ok(Some(mut paths))) => paths.pop(),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let Some(path) = picked else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
this.apply_folder_path(path, window, cx)
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
self.tasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_folder_path(&mut self, path: PathBuf, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let path_string = path.to_string_lossy().to_string();
|
||||||
|
let info = Repo::try_open(&path).and_then(|repo| {
|
||||||
|
let branches = repo.branches().ok()?;
|
||||||
|
let current = repo.current_branch();
|
||||||
|
Some((branches, current))
|
||||||
|
});
|
||||||
|
self.apply_checkout(path_string, info, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_checkout(
|
||||||
|
&mut self,
|
||||||
|
path: String,
|
||||||
|
info: Option<(Vec<String>, Option<String>)>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.fork = None;
|
||||||
|
let Some((branches, current)) = info else {
|
||||||
|
self.error = Some("The chosen folder is not a git repository".into());
|
||||||
|
self.repo_path = None;
|
||||||
|
self.branches.clear();
|
||||||
|
self.merge_base = None;
|
||||||
|
self.commits = None;
|
||||||
|
self.pane.update(cx, |pane, cx| pane.clear(cx));
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if branches.is_empty() {
|
||||||
|
self.error = Some("The repository has no branches yet".into());
|
||||||
|
self.repo_path = None;
|
||||||
|
self.branches.clear();
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let announced = self.store.read(cx).head.clone();
|
||||||
|
let base = announced
|
||||||
|
.as_ref()
|
||||||
|
.filter(|branch| branches.contains(branch))
|
||||||
|
.cloned()
|
||||||
|
.or_else(|| branches.iter().find(|branch| *branch == "main").cloned())
|
||||||
|
.unwrap_or_else(|| branches[0].clone());
|
||||||
|
let compare = current
|
||||||
|
.filter(|branch| branches.contains(branch))
|
||||||
|
.unwrap_or_else(|| base.clone());
|
||||||
|
|
||||||
|
self.repo_path = Some(PathBuf::from(&path));
|
||||||
|
self.error = None;
|
||||||
|
self.branches = branches.into_iter().map(SharedString::from).collect();
|
||||||
|
|
||||||
|
if let Some(addr) = self.store.read(cx).addr().cloned() {
|
||||||
|
CheckoutsStore::global(cx).update(cx, |store, cx| {
|
||||||
|
store.record(PathBuf::from(&path), addr, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.base = SharedString::from(base);
|
||||||
|
self.compare = SharedString::from(compare);
|
||||||
|
self.sync_selectors(window, cx);
|
||||||
|
self.reload_compare(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_selectors(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let options: Vec<(SharedString, SharedString)> = self
|
||||||
|
.branches
|
||||||
|
.iter()
|
||||||
|
.map(|branch| (branch.clone(), branch.clone()))
|
||||||
|
.collect();
|
||||||
|
let base = self.base.clone();
|
||||||
|
let compare = self.compare.clone();
|
||||||
|
|
||||||
|
self.base_select.update(cx, |state, cx| {
|
||||||
|
state.set_options(options.clone(), cx);
|
||||||
|
state.set_selected(Some(base), window, cx);
|
||||||
|
});
|
||||||
|
self.compare_select.update(cx, |state, cx| {
|
||||||
|
state.set_options(options, cx);
|
||||||
|
state.set_selected(Some(compare), window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fork_candidates(&self, cx: &App) -> Vec<Announcement> {
|
||||||
|
let Some(addr) = self.store.read(cx).addr().cloned() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let euc = self
|
||||||
|
.store
|
||||||
|
.read(cx)
|
||||||
|
.announcement
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|a| a.euc.clone());
|
||||||
|
let user = Backend::global(cx).read(cx).current_user();
|
||||||
|
let announcements = RepoListStore::global(cx).read(cx).announcements.clone();
|
||||||
|
Announcement::forks_in(&announcements, &addr, euc.as_deref(), user)
|
||||||
|
.into_iter()
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn choose_fork(
|
||||||
|
&mut self,
|
||||||
|
announcement: Announcement,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let Some(base) = self.store.read(cx).addr().cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mirror_path = Mirrors::path(&base);
|
||||||
|
let namespace = GitCache::fork_namespace(&announcement);
|
||||||
|
let clone_urls: Vec<Url> = announcement.clone.clone();
|
||||||
|
let base_clone_urls: Vec<Url> = self
|
||||||
|
.store
|
||||||
|
.read(cx)
|
||||||
|
.announcement
|
||||||
|
.as_ref()
|
||||||
|
.map(|a| a.clone.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let expected = self.fork.as_ref().map(|fork| fork.announcement.addr());
|
||||||
|
let handle = window.window_handle();
|
||||||
|
|
||||||
|
self.loading = true;
|
||||||
|
self.error = None;
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
let task: gpui::Task<Result<(), anyhow::Error>> = cx.spawn(async move |this, cx| {
|
||||||
|
let result = cx
|
||||||
|
.background_spawn({
|
||||||
|
let namespace = namespace.clone();
|
||||||
|
let mirror_path = mirror_path.clone();
|
||||||
|
async move {
|
||||||
|
Mirrors::ensure(&base, &base_clone_urls)?;
|
||||||
|
let repo = Repo::open(&mirror_path)?;
|
||||||
|
repo.delete_refs_with_prefix("refs/fork")?;
|
||||||
|
repo.fetch_refs(
|
||||||
|
&clone_urls,
|
||||||
|
&format!("+refs/heads/*:refs/fork/{namespace}/*"),
|
||||||
|
)?;
|
||||||
|
let strip = |refs: Vec<String>, prefix: &str| {
|
||||||
|
let mut names: Vec<String> = refs
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|name| {
|
||||||
|
name.strip_prefix(prefix)
|
||||||
|
.map(|rest| rest.trim_start_matches('/').to_owned())
|
||||||
|
})
|
||||||
|
.filter(|name| !name.is_empty())
|
||||||
|
.collect();
|
||||||
|
names.sort();
|
||||||
|
names
|
||||||
|
};
|
||||||
|
let base_branches = strip(
|
||||||
|
repo.refs_with_prefix("refs/remotes/origin")?,
|
||||||
|
"refs/remotes/origin",
|
||||||
|
);
|
||||||
|
let compare_branches = strip(
|
||||||
|
repo.refs_with_prefix(&format!("refs/fork/{namespace}"))?,
|
||||||
|
&format!("refs/fork/{namespace}"),
|
||||||
|
);
|
||||||
|
Ok::<_, anyhow::Error>((base_branches, compare_branches))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(error) = cx.update_window(handle, |_, window, cx| {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
let applied = this.fork.as_ref().map(|fork| fork.announcement.addr());
|
||||||
|
if applied != expected {
|
||||||
|
this.loading = false;
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.apply_fork(announcement, mirror_path, namespace, result, window, cx);
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
log::warn!("failed to apply fork: {error}");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
self.tasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_fork(
|
||||||
|
&mut self,
|
||||||
|
announcement: Announcement,
|
||||||
|
mirror_path: PathBuf,
|
||||||
|
namespace: String,
|
||||||
|
result: Result<(Vec<String>, Vec<String>), anyhow::Error>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.loading = false;
|
||||||
|
let (base_branches, compare_branches) = match result {
|
||||||
|
Ok(branches) => branches,
|
||||||
|
Err(error) => {
|
||||||
|
self.error = Some(format!("Could not compare against the fork: {error}").into());
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if compare_branches.is_empty() {
|
||||||
|
self.error = Some("The fork has no branches to compare".into());
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if base_branches.is_empty() {
|
||||||
|
self.error = Some("Could not list the target repository's branches.".into());
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let announced = self.store.read(cx).head.clone();
|
||||||
|
let base = announced
|
||||||
|
.filter(|branch| base_branches.contains(branch))
|
||||||
|
.map(SharedString::from)
|
||||||
|
.or_else(|| {
|
||||||
|
base_branches
|
||||||
|
.iter()
|
||||||
|
.find(|branch| branch.as_str() == "main")
|
||||||
|
.map(|branch| SharedString::from(branch.clone()))
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| SharedString::from(base_branches[0].clone()));
|
||||||
|
let compare = compare_branches
|
||||||
|
.iter()
|
||||||
|
.find(|branch| branch.as_str() == "main")
|
||||||
|
.map(|branch| SharedString::from(branch.clone()))
|
||||||
|
.unwrap_or_else(|| SharedString::from(compare_branches[0].clone()));
|
||||||
|
|
||||||
|
self.fork = Some(ForkCompare {
|
||||||
|
announcement,
|
||||||
|
namespace,
|
||||||
|
mirror_path,
|
||||||
|
});
|
||||||
|
self.error = None;
|
||||||
|
self.base = base;
|
||||||
|
self.compare = compare;
|
||||||
|
self.branches = base_branches.into_iter().map(SharedString::from).collect();
|
||||||
|
self.sync_selectors(window, cx);
|
||||||
|
self.reload_compare(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reload_compare(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let Some(repo_path) = self.work_path() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let base = self.base_ref();
|
||||||
|
let compare = self.compare_ref();
|
||||||
|
let base_name = self.base.to_string();
|
||||||
|
let compare_name = self.compare.to_string();
|
||||||
|
|
||||||
|
self.loading = true;
|
||||||
|
self.error = None;
|
||||||
|
self.compare_generation += 1;
|
||||||
|
let generation = self.compare_generation;
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
if base == compare {
|
||||||
|
self.loading = false;
|
||||||
|
self.merge_base = None;
|
||||||
|
self.commits = None;
|
||||||
|
self.pane.update(cx, |pane, cx| pane.clear(cx));
|
||||||
|
self.error = Some("Choose different base and compare branches".into());
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let task: gpui::Task<Result<(), anyhow::Error>> = cx.spawn(async move |this, cx| {
|
||||||
|
let result = cx
|
||||||
|
.background_spawn({
|
||||||
|
async move {
|
||||||
|
let repo = Repo::open(Path::new(&repo_path))?;
|
||||||
|
let merge_base = repo.merge_base(&base, &compare)?.ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"{base_name} and {compare_name} share no common ancestor"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let commits = repo.commit_range(&merge_base, &compare)?;
|
||||||
|
let diff = repo.range_diff(&merge_base, &compare)?;
|
||||||
|
Ok::<_, anyhow::Error>((merge_base, commits, diff))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
if generation != this.compare_generation {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
match result {
|
||||||
|
Ok((merge_base, commits, diff)) => {
|
||||||
|
this.merge_base = Some(merge_base);
|
||||||
|
this.commits = Some(commits);
|
||||||
|
this.pane.update(cx, |pane, cx| pane.set_diff(diff, cx));
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
this.merge_base = None;
|
||||||
|
this.commits = None;
|
||||||
|
this.pane.update(cx, |pane, cx| pane.clear(cx));
|
||||||
|
this.error = Some(error.to_string().into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
self.tasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn submit(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.submitting || self.loading {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(merge_base) = self.merge_base.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(repo_path) = self.work_path() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let subject = self.subject.read(cx).value().to_string();
|
||||||
|
let description = self.description.read(cx).value().to_string();
|
||||||
|
let branch_name = self.compare.to_string();
|
||||||
|
let compare_ref = self.compare_ref();
|
||||||
|
let store = self.store.clone();
|
||||||
|
let dock = self.dock.clone();
|
||||||
|
let panel_id = self.panel_id.clone();
|
||||||
|
|
||||||
|
self.submitting = true;
|
||||||
|
self.error = None;
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
let task: gpui::Task<Result<(), anyhow::Error>> = cx.spawn(async move |this, cx| {
|
||||||
|
let publish = store.update(cx, |store, cx| {
|
||||||
|
store.open_pull_request_from_refs(
|
||||||
|
repo_path,
|
||||||
|
merge_base,
|
||||||
|
compare_ref,
|
||||||
|
(!subject.is_empty()).then_some(subject),
|
||||||
|
description,
|
||||||
|
Some(branch_name),
|
||||||
|
false,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if let Err(error) = publish.await {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.submitting = false;
|
||||||
|
this.error = Some(error.to_string().into());
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
this.update(cx, |this, _cx| {
|
||||||
|
this.submitting = false;
|
||||||
|
})?;
|
||||||
|
let _ = dock.update(cx, |dock, cx| dock.close_panel(&panel_id, cx));
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
self.tasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_commit_diff(&mut self, commit_id: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(repo_path) = self.work_path() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let id = match self.store.read(cx).addr() {
|
||||||
|
Some(addr) => SharedString::from(format!("commit:{addr}:{commit_id}")),
|
||||||
|
None => SharedString::from(format!("commit:{}:{commit_id}", repo_path.display())),
|
||||||
|
};
|
||||||
|
let dock = self.dock.clone();
|
||||||
|
let store = self.store.clone();
|
||||||
|
let repo_name = self.repo_name.clone();
|
||||||
|
let commit_id = commit_id.to_owned();
|
||||||
|
crate::shared::panels::open(
|
||||||
|
&dock,
|
||||||
|
id,
|
||||||
|
move |window, cx| {
|
||||||
|
cx.new(|cx| {
|
||||||
|
CommitDiffView::new(
|
||||||
|
store.clone(),
|
||||||
|
repo_path.clone(),
|
||||||
|
repo_name.clone(),
|
||||||
|
commit_id.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn submit_enabled(&self, cx: &App) -> bool {
|
||||||
|
self.has_source()
|
||||||
|
&& !self.loading
|
||||||
|
&& !self.submitting
|
||||||
|
&& self.merge_base.is_some()
|
||||||
|
&& self
|
||||||
|
.commits
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|commits| !commits.is_empty())
|
||||||
|
&& !self.subject.read(cx).value().is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_compare_bar(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
|
let weak = cx.entity().downgrade();
|
||||||
|
let weak_menu = cx.entity().downgrade();
|
||||||
|
let source_items = self.source_menu_items(cx, cx.entity().downgrade());
|
||||||
|
let can_submit = self.submit_enabled(cx);
|
||||||
|
let source_label = match &self.fork {
|
||||||
|
Some(fork) => truncate_label(&fork_display_name(&fork.announcement)),
|
||||||
|
None => self.repo_path.as_ref().map_or_else(
|
||||||
|
|| SharedString::from("No source"),
|
||||||
|
|path| truncate_label(&path.display().to_string()),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let refresh_fork = self.fork.as_ref().map(|fork| fork.announcement.clone());
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.px_4()
|
||||||
|
.py_2()
|
||||||
|
.w_full()
|
||||||
|
.gap_3()
|
||||||
|
.items_end()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(label("Merge Into"))
|
||||||
|
.child(div().w(px(160.)).child(self.base_select.clone())),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(label("Pull From"))
|
||||||
|
.child(div().w(px(160.)).child(self.compare_select.clone())),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex().gap_1().child(label("Source")).child(
|
||||||
|
Menu::new("pr-source")
|
||||||
|
.open(self.source_open)
|
||||||
|
.on_change(move |open, _, cx| {
|
||||||
|
let _ = weak_menu.update(cx, |this, cx| {
|
||||||
|
this.source_open = open;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.trigger(move |_, _| {
|
||||||
|
Button::new("compare-source")
|
||||||
|
.label(source_label.clone())
|
||||||
|
.variant(gpui_native_kit::button::ButtonVariant::Secondary)
|
||||||
|
.into_any_element()
|
||||||
|
})
|
||||||
|
.items(move |_, _| source_items.clone()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.when_some(refresh_fork, |this, fork| {
|
||||||
|
this.child(
|
||||||
|
Button::new("refresh-fork")
|
||||||
|
.icon(Icon::named(CustomIconName::Refresh))
|
||||||
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
this.choose_fork(fork.clone(), window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.child(div().flex_1())
|
||||||
|
.child(
|
||||||
|
Button::new("create-pr")
|
||||||
|
.label("Create pull request")
|
||||||
|
.icon(Icon::named(IconRole::Plus))
|
||||||
|
.loading(self.submitting)
|
||||||
|
.disabled(!can_submit)
|
||||||
|
.on_click(move |_, window, cx| {
|
||||||
|
let _ = weak.update(cx, |this, cx| this.submit(window, cx));
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_menu_items(&self, cx: &App, view: WeakEntity<Self>) -> Vec<MenuItem> {
|
||||||
|
let associated = self
|
||||||
|
.store
|
||||||
|
.read(cx)
|
||||||
|
.addr()
|
||||||
|
.map(|addr| CheckoutsStore::global(cx).read(cx).associations_of(addr))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let active_path = (self.fork.is_none())
|
||||||
|
.then(|| self.repo_path.clone())
|
||||||
|
.flatten();
|
||||||
|
let active_fork = self.fork.as_ref().map(|fork| fork.announcement.addr());
|
||||||
|
|
||||||
|
let mut items = Vec::new();
|
||||||
|
for path in associated {
|
||||||
|
let label = path
|
||||||
|
.file_name()
|
||||||
|
.map(|name| name.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| path.display().to_string());
|
||||||
|
let view = view.clone();
|
||||||
|
items.push(
|
||||||
|
MenuItem::new(truncate_label(&label))
|
||||||
|
.checked(active_path.as_ref() == Some(&path))
|
||||||
|
.handler(move |window, cx| {
|
||||||
|
let path = path.clone();
|
||||||
|
let _ =
|
||||||
|
view.update(cx, |this, cx| this.apply_folder_path(path, window, cx));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let folder_view = view.clone();
|
||||||
|
items.push(
|
||||||
|
MenuItem::new("Choose another folder…").handler(move |window, cx| {
|
||||||
|
let _ = folder_view.update(cx, |this, cx| this.choose_checkout(window, cx));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
items.push(MenuItem::separator());
|
||||||
|
|
||||||
|
let candidates = self.fork_candidates(cx);
|
||||||
|
if candidates.is_empty() {
|
||||||
|
items.push(MenuItem::new("No announced forks of this repository").disabled(true));
|
||||||
|
} else {
|
||||||
|
let user = Backend::global(cx).read(cx).current_user();
|
||||||
|
for candidate in candidates {
|
||||||
|
let subtitle = if Some(candidate.owner) == user {
|
||||||
|
SharedString::from("Your fork")
|
||||||
|
} else {
|
||||||
|
SharedString::from(format!("by {}", shorten_owner(&candidate.owner)))
|
||||||
|
};
|
||||||
|
let title = format!("{} — {}", fork_display_name(&candidate), subtitle);
|
||||||
|
let view = view.clone();
|
||||||
|
items.push(
|
||||||
|
MenuItem::new(truncate_label(&title))
|
||||||
|
.checked(active_fork == Some(candidate.addr()))
|
||||||
|
.handler(move |window, cx| {
|
||||||
|
let announcement = candidate.clone();
|
||||||
|
let _ = view
|
||||||
|
.update(cx, |this, cx| this.choose_fork(announcement, window, cx));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_tabs(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
|
let files = self.pane.read(cx).diff().map_or(0, |diff| diff.files.len());
|
||||||
|
let commits = self.commits.as_ref().map_or(0, |commits| commits.len());
|
||||||
|
let weak = cx.entity().downgrade();
|
||||||
|
|
||||||
|
Tabs::new("new-pr-tabs")
|
||||||
|
.selected(self.active_tab)
|
||||||
|
.tabs(move |_, _| {
|
||||||
|
vec![
|
||||||
|
Tab::new("files", format!("Files ({files})"))
|
||||||
|
.lead(Icon::named(CustomIconName::GitFile)),
|
||||||
|
Tab::new("commits", format!("Commits ({commits})"))
|
||||||
|
.lead(Icon::named(CustomIconName::GitCommit)),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.on_change(move |index, _, cx| {
|
||||||
|
let _ = weak.update(cx, |this, cx| {
|
||||||
|
this.active_tab = index;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_content(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
|
if self.loading {
|
||||||
|
return v_flex()
|
||||||
|
.size_full()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.child(Spinner::new("new-pr-loading"))
|
||||||
|
.into_any_element();
|
||||||
|
}
|
||||||
|
if !self.has_source() {
|
||||||
|
return empty_state("Choose a local checkout or an announced fork to compare");
|
||||||
|
}
|
||||||
|
if self.commits.is_none() && self.error.is_some() {
|
||||||
|
return empty_state("Nothing to compare");
|
||||||
|
}
|
||||||
|
match self.active_tab {
|
||||||
|
0 => self.pane.clone().into_any_element(),
|
||||||
|
_ => self.render_commits(cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_commits(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
|
let Some(commits) = self.commits.as_ref() else {
|
||||||
|
return empty_state("No commits");
|
||||||
|
};
|
||||||
|
if commits.is_empty() {
|
||||||
|
return empty_state("No commits between the branches");
|
||||||
|
}
|
||||||
|
let commits = commits.clone();
|
||||||
|
let view = cx.entity().downgrade();
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.relative()
|
||||||
|
.flex_1()
|
||||||
|
.min_h_0()
|
||||||
|
.child(
|
||||||
|
uniform_list("new-pr-commits", commits.len(), move |range, _, cx| {
|
||||||
|
range
|
||||||
|
.map(|ix| {
|
||||||
|
let commit = commits[ix].clone();
|
||||||
|
let id = commit.id.clone();
|
||||||
|
let view = view.clone();
|
||||||
|
commit_row(
|
||||||
|
ix,
|
||||||
|
&commit,
|
||||||
|
move |window, cx| {
|
||||||
|
let _ = view.update(cx, |this, cx| {
|
||||||
|
this.open_commit_diff(&id, window, cx);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.track_scroll(&self.scroll_handle)
|
||||||
|
.size_full(),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.top_0()
|
||||||
|
.left_0()
|
||||||
|
.right_0()
|
||||||
|
.bottom_0()
|
||||||
|
.child(Scrollbar::vertical(
|
||||||
|
"new-pr-commits-scroll",
|
||||||
|
self.scroll_handle.clone(),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Panel for NewPullRequestView {
|
||||||
|
fn title(&self, _cx: &gpui::App) -> Option<SharedString> {
|
||||||
|
Some(SharedString::from(format!(
|
||||||
|
"{}/new-pull-request",
|
||||||
|
self.repo_name
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lead(&self, cx: &gpui::App) -> Option<AnyElement> {
|
||||||
|
Some(crate::shared::widgets::repo_tab_avatar(
|
||||||
|
self.store.read(cx),
|
||||||
|
&self.directory,
|
||||||
|
cx,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for NewPullRequestView {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.id("new-pr")
|
||||||
|
.size_full()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_3()
|
||||||
|
.child(self.render_compare_bar(cx))
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.px_4()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
Input::new("pr-title", self.subject.clone()).placeholder("Title"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Input::new("pr-description", self.description.clone())
|
||||||
|
.rows(3)
|
||||||
|
.placeholder("Describe..."),
|
||||||
|
)
|
||||||
|
.when_some(self.error.clone(), |this, error| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().ramps.destructive.base)
|
||||||
|
.child(error),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(self.render_tabs(cx)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.flex_1()
|
||||||
|
.min_h_0()
|
||||||
|
.w_full()
|
||||||
|
.child(self.render_content(cx)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(text: &'static str) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||||
|
.child(text)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
use assets::CustomIconName;
|
||||||
|
use gpui::prelude::FluentBuilder as _;
|
||||||
|
use gpui::{
|
||||||
|
AnyElement, App, Context, Entity, InteractiveElement as _, IntoElement, ParentElement as _,
|
||||||
|
Render, SharedString, Styled as _, WeakEntity, Window, div,
|
||||||
|
};
|
||||||
|
use gpui_native_kit::button::Button;
|
||||||
|
use gpui_native_kit::common::{h_flex, v_flex};
|
||||||
|
use gpui_native_kit::icons::Icon;
|
||||||
|
use gpui_native_kit::input::{Input, InputState};
|
||||||
|
use gpui_native_kit::layout::dock::{DockState, Panel};
|
||||||
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
|
use metadata::ProfileDirectory;
|
||||||
|
use signed_state::RepoStore;
|
||||||
|
|
||||||
|
use crate::shared::widgets::repo_tab_avatar;
|
||||||
|
|
||||||
|
pub struct SendPatchView {
|
||||||
|
dock: WeakEntity<DockState>,
|
||||||
|
panel_id: SharedString,
|
||||||
|
store: Entity<RepoStore>,
|
||||||
|
repo_name: SharedString,
|
||||||
|
subject: Entity<InputState>,
|
||||||
|
description: Entity<InputState>,
|
||||||
|
patch: Entity<InputState>,
|
||||||
|
submitting: bool,
|
||||||
|
error: Option<SharedString>,
|
||||||
|
directory: Entity<ProfileDirectory>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SendPatchView {
|
||||||
|
pub fn new(
|
||||||
|
dock: WeakEntity<DockState>,
|
||||||
|
panel_id: SharedString,
|
||||||
|
store: Entity<RepoStore>,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let repo_name = store.read(cx).name();
|
||||||
|
let directory = ProfileDirectory::new(cx);
|
||||||
|
|
||||||
|
if let Some(owner) = store.read(cx).announcement.as_ref().map(|a| a.owner) {
|
||||||
|
directory.update(cx, |directory, cx| directory.track([owner], cx));
|
||||||
|
}
|
||||||
|
|
||||||
|
let subject = InputState::new(cx);
|
||||||
|
|
||||||
|
let description = InputState::new(cx);
|
||||||
|
description.update(cx, |state, cx| state.set_multiline(true, cx));
|
||||||
|
|
||||||
|
let patch = InputState::new(cx);
|
||||||
|
patch.update(cx, |state, cx| state.set_multiline(true, cx));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
dock,
|
||||||
|
panel_id,
|
||||||
|
store,
|
||||||
|
repo_name,
|
||||||
|
subject,
|
||||||
|
description,
|
||||||
|
patch,
|
||||||
|
submitting: false,
|
||||||
|
error: None,
|
||||||
|
directory,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn submit(&mut self, 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.submitting = true;
|
||||||
|
self.error = None;
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
let sync_error = self.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
let panel_id = self.panel_id.clone();
|
||||||
|
let _ = self
|
||||||
|
.dock
|
||||||
|
.update(cx, |dock, cx| dock.close_panel(&panel_id, cx));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_footer(&self, cx: &Context<Self>) -> AnyElement {
|
||||||
|
let can_submit = !self.submitting
|
||||||
|
&& !self.subject.read(cx).value().is_empty()
|
||||||
|
&& !self.patch.read(cx).value().is_empty();
|
||||||
|
let weak = cx.entity().downgrade();
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.px_4()
|
||||||
|
.py_2()
|
||||||
|
.w_full()
|
||||||
|
.justify_end()
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(cx.theme().colors.border)
|
||||||
|
.child(
|
||||||
|
Button::new("send-patch")
|
||||||
|
.label("Send patch")
|
||||||
|
.icon(Icon::named(CustomIconName::ArrowUp))
|
||||||
|
.loading(self.submitting)
|
||||||
|
.disabled(!can_submit)
|
||||||
|
.on_click(move |_, _, cx| {
|
||||||
|
let _ = weak.update(cx, |this, cx| this.submit(cx));
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Panel for SendPatchView {
|
||||||
|
fn title(&self, _cx: &App) -> Option<SharedString> {
|
||||||
|
Some(SharedString::from(format!("{}/send-patch", self.repo_name)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lead(&self, cx: &App) -> Option<AnyElement> {
|
||||||
|
Some(repo_tab_avatar(self.store.read(cx), &self.directory, cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
v_flex()
|
||||||
|
.p_4()
|
||||||
|
.flex_1()
|
||||||
|
.gap_2()
|
||||||
|
.child(Input::new("patch-subject", self.subject.clone()).placeholder("Title"))
|
||||||
|
.child(
|
||||||
|
Input::new("patch-description", self.description.clone())
|
||||||
|
.rows(2)
|
||||||
|
.placeholder("Describe the change"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().colors.muted_foreground)
|
||||||
|
.child(
|
||||||
|
"You can paste a git diff or a git format-patch patch series here.",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Input::new("patch-body", self.patch.clone())
|
||||||
|
.rows(12)
|
||||||
|
.placeholder("diff --git a/file.txt b/file.txt"),
|
||||||
|
)
|
||||||
|
.when_some(self.error.clone(), |this, error| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().ramps.destructive.base)
|
||||||
|
.child(error),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(self.render_footer(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,6 +92,17 @@ impl DiffPane {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.diff = None;
|
||||||
|
self.seeds.clear();
|
||||||
|
self.expanded.clear();
|
||||||
|
self.rows.clear();
|
||||||
|
self.selected_file = None;
|
||||||
|
self.selected_index = None;
|
||||||
|
self.diff_rows.clear();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
fn select_file(&mut self, path: &str, cx: &mut Context<Self>) {
|
fn select_file(&mut self, path: &str, cx: &mut Context<Self>) {
|
||||||
self.selected_file = Some(path.into());
|
self.selected_file = Some(path.into());
|
||||||
self.selected_index = self.rows.iter().position(|row| row.id == path);
|
self.selected_index = self.rows.iter().position(|row| row.id == path);
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ fn render_diff_line(line: &DiffLine, cx: &App) -> AnyElement {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn commit_row(
|
pub(crate) fn commit_row(
|
||||||
ix: usize,
|
ix: usize,
|
||||||
commit: &FileCommit,
|
commit: &FileCommit,
|
||||||
on_click: impl Fn(&mut Window, &mut gpui::App) + 'static,
|
on_click: impl Fn(&mut Window, &mut gpui::App) + 'static,
|
||||||
|
|||||||
@@ -42,9 +42,6 @@ pub mod helpers;
|
|||||||
pub mod history;
|
pub mod history;
|
||||||
pub mod refs;
|
pub mod refs;
|
||||||
|
|
||||||
use crate::issues::IssuesView;
|
|
||||||
use crate::shared::tree::{build_tree_items, sorted_worktree_paths};
|
|
||||||
use crate::shared::widgets::repo_tab_avatar;
|
|
||||||
use banners::Banners;
|
use banners::Banners;
|
||||||
use files::RepoFilesView;
|
use files::RepoFilesView;
|
||||||
use helpers::{
|
use helpers::{
|
||||||
@@ -54,6 +51,11 @@ use helpers::{
|
|||||||
use history::RepoHistoryView;
|
use history::RepoHistoryView;
|
||||||
use refs::RefSwitcher;
|
use refs::RefSwitcher;
|
||||||
|
|
||||||
|
use crate::issues::IssuesView;
|
||||||
|
use crate::pull_requests::{NewPullRequestView, PullRequestsView, SendPatchView};
|
||||||
|
use crate::shared::tree::{build_tree_items, sorted_worktree_paths};
|
||||||
|
use crate::shared::widgets::repo_tab_avatar;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum RefKind {
|
pub enum RefKind {
|
||||||
/// HEAD stays attached.
|
/// HEAD stays attached.
|
||||||
@@ -89,6 +91,7 @@ pub struct RepoDetailView {
|
|||||||
repo_menu_open: bool,
|
repo_menu_open: bool,
|
||||||
clone_open: bool,
|
clone_open: bool,
|
||||||
issues_menu_open: bool,
|
issues_menu_open: bool,
|
||||||
|
pulls_menu_open: bool,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,13 +149,15 @@ impl RepoDetailView {
|
|||||||
}
|
}
|
||||||
})];
|
})];
|
||||||
|
|
||||||
|
let store_for_attach = store.clone();
|
||||||
window.defer(cx, move |window, cx| {
|
window.defer(cx, move |window, cx| {
|
||||||
if let Err(error) = weak.update(cx, |this, cx| this.load_repo(window, cx)) {
|
let _ = weak.update(cx, |this, cx| {
|
||||||
log::warn!("failed to load repository: {error}");
|
this.attach_store(&store_for_attach, window, cx);
|
||||||
}
|
this.load_repo(window, cx);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut view = Self {
|
Self {
|
||||||
dock,
|
dock,
|
||||||
store: store.clone(),
|
store: store.clone(),
|
||||||
repo_started: false,
|
repo_started: false,
|
||||||
@@ -172,11 +177,9 @@ impl RepoDetailView {
|
|||||||
repo_menu_open: false,
|
repo_menu_open: false,
|
||||||
clone_open: false,
|
clone_open: false,
|
||||||
issues_menu_open: false,
|
issues_menu_open: false,
|
||||||
|
pulls_menu_open: false,
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
};
|
}
|
||||||
|
|
||||||
view.attach_store(&store, window, cx);
|
|
||||||
view
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn apply_announcement(
|
pub(crate) fn apply_announcement(
|
||||||
@@ -828,6 +831,85 @@ impl RepoDetailView {
|
|||||||
dialogs::init::open(local_path, cx.entity().downgrade(), window, cx);
|
dialogs::init::open(local_path, cx.entity().downgrade(), window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_pulls(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(addr) = self.store.read(cx).addr().cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let id = SharedString::from(format!("pulls:{addr}"));
|
||||||
|
let dock = self.dock.clone();
|
||||||
|
let dock_for_build = dock.clone();
|
||||||
|
let store = self.store.clone();
|
||||||
|
|
||||||
|
crate::shared::panels::open(
|
||||||
|
&dock,
|
||||||
|
id,
|
||||||
|
move |_, cx| {
|
||||||
|
cx.new(|cx| PullRequestsView::new(dock_for_build.clone(), store.clone(), cx))
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_new_pull_request(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(addr) = self.store.read(cx).addr().cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let id = SharedString::from(format!("new-pr:{addr}"));
|
||||||
|
let dock = self.dock.clone();
|
||||||
|
let dock_for_build = dock.clone();
|
||||||
|
let store = self.store.clone();
|
||||||
|
|
||||||
|
crate::shared::panels::open(
|
||||||
|
&dock,
|
||||||
|
id.clone(),
|
||||||
|
move |window, cx| {
|
||||||
|
cx.new(|cx| {
|
||||||
|
NewPullRequestView::new(
|
||||||
|
dock_for_build.clone(),
|
||||||
|
id.clone(),
|
||||||
|
store.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_send_patch(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(addr) = self.store.read(cx).addr().cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let id = SharedString::from(format!("send-patch:{addr}"));
|
||||||
|
let dock = self.dock.clone();
|
||||||
|
let dock_for_build = dock.clone();
|
||||||
|
let store = self.store.clone();
|
||||||
|
|
||||||
|
crate::shared::panels::open(
|
||||||
|
&dock,
|
||||||
|
id.clone(),
|
||||||
|
move |window, cx| {
|
||||||
|
cx.new(|cx| {
|
||||||
|
SendPatchView::new(
|
||||||
|
dock_for_build.clone(),
|
||||||
|
id.clone(),
|
||||||
|
store.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn open_issues(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn open_issues(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Some(addr) = self.store.read(cx).addr().cloned() else {
|
let Some(addr) = self.store.read(cx).addr().cloned() else {
|
||||||
return;
|
return;
|
||||||
@@ -944,6 +1026,53 @@ impl RepoDetailView {
|
|||||||
.flex_none()
|
.flex_none()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.justify_end()
|
.justify_end()
|
||||||
|
.child({
|
||||||
|
let weak_pulls = cx.entity().downgrade();
|
||||||
|
let weak_pulls_open = cx.entity().downgrade();
|
||||||
|
let _store_for_pulls = self.store.clone();
|
||||||
|
let pulls_count = self.store.read(cx).pull_request_count();
|
||||||
|
|
||||||
|
DropdownMenu::new("pulls")
|
||||||
|
.label(format!("Pull requests {pulls_count}"))
|
||||||
|
.icon(Icon::named(CustomIconName::GitPullRequest))
|
||||||
|
.variant(ButtonVariant::Secondary)
|
||||||
|
.open(self.pulls_menu_open)
|
||||||
|
.on_click(move |_, window, cx| {
|
||||||
|
if let Err(error) = weak_pulls.update(cx, |this, cx| {
|
||||||
|
this.open_pulls(window, cx);
|
||||||
|
}) {
|
||||||
|
log::warn!("failed to open pull requests: {error}");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_open_change(move |open, _, cx| {
|
||||||
|
if let Err(error) = weak_pulls_open.update(cx, |this, cx| {
|
||||||
|
this.pulls_menu_open = open;
|
||||||
|
cx.notify();
|
||||||
|
}) {
|
||||||
|
log::warn!("failed to update pulls menu: {error}");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.items({
|
||||||
|
let weak_new = cx.entity().downgrade();
|
||||||
|
let weak_patch = cx.entity().downgrade();
|
||||||
|
move |_, _| {
|
||||||
|
let weak_new = weak_new.clone();
|
||||||
|
let weak_patch = weak_patch.clone();
|
||||||
|
vec![
|
||||||
|
MenuItem::new("New pull request").handler(move |window, cx| {
|
||||||
|
let _ = weak_new.update(cx, |this, cx| {
|
||||||
|
this.open_new_pull_request(window, cx);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
MenuItem::new("Send patch").handler(move |window, cx| {
|
||||||
|
let _ = weak_patch.update(cx, |this, cx| {
|
||||||
|
this.open_send_patch(window, cx);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
.child({
|
.child({
|
||||||
let weak_issues = cx.entity().downgrade();
|
let weak_issues = cx.entity().downgrade();
|
||||||
let weak_issues_open = cx.entity().downgrade();
|
let weak_issues_open = cx.entity().downgrade();
|
||||||
|
|||||||
Reference in New Issue
Block a user