update pull request

This commit is contained in:
2026-09-03 10:44:38 +07:00
parent 33cbe42551
commit 01f0540726
16 changed files with 3211 additions and 248 deletions
@@ -82,10 +82,11 @@ impl IssuesView {
pub fn new(
dock_area: WeakEntity<DockArea>,
store: Entity<RepoStore>,
repo_name: SharedString,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let repo_name = store.read(cx).name();
Self {
focus_handle: cx.focus_handle(),
dock_area,
+165 -28
View File
@@ -27,9 +27,12 @@ use gpui_component::{
VirtualListScrollHandle, h_flex, v_flex,
};
use nostr::prelude::{EventId, RelayUrl, ToBech32};
use signed_core::{Announcement, RepoAddr, filters};
use signed_core::{Announcement, RepoAddr, RepoStatus, filters};
use signed_git::{CommitList, FileCommit};
use signed_state::{Backend, GitStore, LocalReposStore, ProfileStore, RepoListStore, RepoStore};
use signed_state::{
Backend, CheckoutStatus, CheckoutsStore, GitStore, LocalReposStore, ProfileStore,
RepoListStore, RepoStore, pr_proposes_checkout,
};
use signed_ui::image_cache::{MAX_IMAGES, image_cache};
use signed_ui::{DropdownButton, PixelAvatar, UserAvatar, copy_row};
@@ -198,6 +201,17 @@ pub struct RepoDetailView {
tasks: Vec<Task<Result<(), Error>>>,
/// Subscriptions keeping the selectors' confirm events alive.
_subscriptions: Vec<Subscription>,
/// Observes the checkouts store, whose statuses feed the "ready to
/// contribute" banner of the repository panel.
_checkouts_subscription: Subscription,
/// `(path, branch)` ready-suggestions dismissed by the user, per panel.
banner_dismissed: HashSet<(PathBuf, String)>,
/// The announced HEAD the ready-statuses were last requested with, and
/// whether they were requested at all (re-requested only when the HEAD
/// — the base default — changes, e.g. when the store's first refresh
/// lands).
ready_requested: bool,
ready_head: Option<String>,
/// Upstream repository (from this fork's `u` tag) the user asked to
/// open, while its announcement is still being fetched.
pending_upstream: Option<RepoAddr>,
@@ -219,7 +233,16 @@ impl RepoDetailView {
let relays = initial.relays.clone();
let store = cx.new(|cx| RepoStore::new(addr, relays, cx));
Self::new_common(dock_area, Some(initial), Some(store), None, window, cx)
let mut view = Self::new_common(
dock_area,
Some(initial),
Some(store.clone()),
None,
window,
cx,
);
view.attach_store(&store, cx);
view
}
/// Open a local repository discovered by the scan. There is no
@@ -328,6 +351,11 @@ impl RepoDetailView {
focus_handle: cx.focus_handle(),
tasks: Vec::new(),
_subscriptions: subscriptions,
_checkouts_subscription: cx
.observe(&CheckoutsStore::global(cx), |_this, _store, cx| cx.notify()),
banner_dismissed: HashSet::new(),
ready_requested: false,
ready_head: None,
pending_upstream: None,
}
}
@@ -548,7 +576,7 @@ impl RepoDetailView {
return;
}
let (clone_urls, name) = {
let (clone_urls, name, addr) = {
let Some(announcement) = self.announcement(cx) else {
return;
};
@@ -569,7 +597,7 @@ impl RepoDetailView {
} else {
name
};
(clone_urls, name)
(clone_urls, name, addr)
};
self.cloning = true;
@@ -599,14 +627,23 @@ impl RepoDetailView {
let destination = folder.join(&name);
let destination_for_open = destination.clone();
let clone_target = destination_for_open.clone();
let result = cx
.background_spawn(async move { signed_git::clone_repo(&clone_urls, &destination) })
.background_spawn(async move { signed_git::clone_repo(&clone_urls, &clone_target) })
.await;
this.update_in(cx, |this, _window, cx| {
this.cloning = false;
match result {
Ok(_) => cx.open_with_system(&destination_for_open),
Ok(_) => {
cx.open_with_system(&destination_for_open);
// Remember the clone as a checkout of this
// repository, so the New PR panel pre-fills it.
let checkouts = CheckoutsStore::global(cx);
checkouts.update(cx, |store, cx| {
store.record(destination, addr, cx);
});
}
Err(error) => {
this.error = Some(format!("Failed to clone: {error}").into());
}
@@ -934,15 +971,7 @@ impl RepoDetailView {
return;
};
let panel = cx.new(|cx| {
IssuesView::new(
self.dock_area.clone(),
store,
self.display_name(cx),
window,
cx,
)
});
let panel = cx.new(|cx| IssuesView::new(self.dock_area.clone(), store, window, cx));
dock_area.update(cx, |dock_area, cx| {
dock_area.add_panel_view(panel_handle(panel), DockPlacement::Center, None, window, cx);
@@ -958,15 +987,7 @@ impl RepoDetailView {
return;
};
let panel = cx.new(|cx| {
PullRequestsView::new(
self.dock_area.clone(),
store,
self.display_name(cx),
window,
cx,
)
});
let panel = cx.new(|cx| PullRequestsView::new(self.dock_area.clone(), store, window, cx));
dock_area.update(cx, |dock_area, cx| {
dock_area.add_panel_view(panel_handle(panel), DockPlacement::Center, None, window, cx);
@@ -1798,14 +1819,127 @@ impl RepoDetailView {
}
let store =
cx.new(|cx| RepoStore::new(announcement.addr(), announcement.relays.clone(), cx));
// Re-render when the store refreshes (issues, PRs, statuses).
self._subscriptions
.push(cx.observe(&store, |_this, _store, cx| cx.notify()));
// Re-render on store refreshes (issues, PRs, statuses) and keep the
// "ready to contribute" statuses of this repository requested.
self.attach_store(&store, cx);
self.store = Some(store);
self.initial = Some(announcement);
cx.notify();
}
/// Observe the repository's store (re-render on refreshes) and request
/// the "ready to contribute" statuses for it.
fn attach_store(&mut self, store: &Entity<RepoStore>, cx: &mut Context<Self>) {
self._subscriptions
.push(cx.observe(store, |this, _store, cx| {
cx.notify();
// The first refresh fills the announced HEAD, which defaults
// the banner's base branch; re-request when it changes.
this.refresh_ready_statuses(cx);
}));
self.refresh_ready_statuses(cx);
}
/// (Re)request the ready statuses of this repository when the announced
/// HEAD — the base the checkouts are compared against — changed since
/// the last request.
fn refresh_ready_statuses(&mut self, cx: &mut Context<Self>) {
let Some(store) = self.store.clone() else {
return;
};
let head = store.read(cx).head.clone();
if self.ready_requested && self.ready_head == head {
return;
}
self.ready_requested = true;
self.ready_head = head.clone();
let addr = store.read(cx).addr().clone();
CheckoutsStore::global(cx).update(cx, |store, cx| {
store.request_statuses(&addr, head, cx);
});
}
/// The first checkout ready for a pull request on this repository,
/// not covered by an open PR of the signed-in user and not dismissed in this panel.
fn ready_suggestion(&self, cx: &App) -> Option<CheckoutStatus> {
let store = self.store.as_ref()?;
let addr = store.read(cx).addr().clone();
let statuses = CheckoutsStore::global(cx).read(cx).statuses_of(&addr);
let user = Backend::global(cx).read(cx).current_user()?;
'status: for status in statuses {
if self
.banner_dismissed
.contains(&(status.path.clone(), status.branch.clone()))
{
continue;
}
let store = store.read(cx);
for pr in &store.pull_requests {
if pr_proposes_checkout(pr, store.status_of(pr) == RepoStatus::Open, user, &status)
{
continue 'status;
}
}
return Some(status);
}
None
}
/// The "ready to contribute" banner of the repository panel: message,
/// a Create action opening the prefilled New PR panel, and a dismiss
/// control.
fn render_ready_banner(&self, cx: &Context<Self>) -> Option<AnyElement> {
let status = self.ready_suggestion(cx)?;
let commits = if status.ahead == 1 {
"1 commit".to_owned()
} else {
format!("{} commits", status.ahead)
};
let message = SharedString::from(format!(
"{} is {} ahead of {} in {}",
status.branch,
commits,
status.base,
status.path.display()
));
let key = (status.path.clone(), status.branch.clone());
let view = cx.entity().clone();
Some(
h_flex()
.gap_2()
.px_4()
.pt_1()
.w_full()
.items_center()
.child(
Alert::info("repo-ready-to-contribute", message)
.banner()
.flex_1()
.on_close(move |_event, _window, cx| {
view.update(cx, |this, _| {
this.banner_dismissed.insert(key.clone());
});
}),
)
.child(
Button::new("create-pr-from-banner")
.small()
.icon(IconName::Plus)
.label("Create pull request")
.on_click(cx.listener(|this, _event, window, cx| {
if let Some(store) = this.store.clone() {
open_new_pull_panel(this.dock_area.clone(), store, window, cx);
}
})),
)
.into_any_element(),
)
}
/// The tab row shared by both header variants: Files/Commits tabs, the
/// HEAD commit button and the branch/tag selectors.
fn render_header_tabs(&self, cx: &mut Context<Self>) -> AnyElement {
@@ -2036,6 +2170,9 @@ impl Render for RepoDetailView {
.id("repo")
.size_full()
.child(self.render_header(cx))
.when_some(self.render_ready_banner(cx), |this, banner| {
this.child(banner)
})
.when_some(self.error.clone(), |this, error| {
this.child(
Alert::error("repo-error", error)
File diff suppressed because it is too large Load Diff
@@ -92,10 +92,11 @@ impl PullRequestsView {
pub fn new(
dock_area: WeakEntity<DockArea>,
store: Entity<RepoStore>,
repo_name: SharedString,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let repo_name = store.read(cx).name();
Self {
focus_handle: cx.focus_handle(),
dock_area,