clean up
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:
@@ -128,7 +128,9 @@ impl Render for OnboardingDialog {
|
||||
.loading(busy)
|
||||
.disabled(busy)
|
||||
.on_click(move |_, _, cx| {
|
||||
let _ = this.update(cx, |this, cx| this.create_identity(cx));
|
||||
crate::shared::log_dropped(
|
||||
this.update(cx, |this, cx| this.create_identity(cx)),
|
||||
);
|
||||
})
|
||||
.into_any_element();
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ impl Render for PassphraseDialog {
|
||||
.loading(busy)
|
||||
.disabled(busy)
|
||||
.on_click(move |_, _, cx| {
|
||||
let _ = this.update(cx, |this, cx| this.unlock(cx));
|
||||
crate::shared::log_dropped(this.update(cx, |this, cx| this.unlock(cx)));
|
||||
})
|
||||
.into_any_element();
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ use metadata::ProfileDirectory;
|
||||
use nostr::prelude::{Event, Kind, PublicKey};
|
||||
use utils::relative_time;
|
||||
|
||||
pub fn repo_header(name: SharedString, unread: usize) -> AnyElement {
|
||||
pub fn repo_header(id: SharedString, name: SharedString, unread: usize) -> AnyElement {
|
||||
h_flex()
|
||||
.h_12()
|
||||
.w_full()
|
||||
@@ -27,7 +27,7 @@ pub fn repo_header(name: SharedString, unread: usize) -> AnyElement {
|
||||
.child(name),
|
||||
)
|
||||
.when(unread > 0, |this| {
|
||||
this.child(Badge::new("inbox-unread", unread.to_string()))
|
||||
this.child(Badge::new(id.clone(), unread.to_string()))
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -27,13 +27,6 @@ use crate::repo::actions::{RepoItem, open_repo_item};
|
||||
|
||||
const MAX_SUB_ACTIVITIES: usize = 5;
|
||||
|
||||
struct InboxSection {
|
||||
address: Option<RepoAddr>,
|
||||
unread: usize,
|
||||
entries: Vec<usize>,
|
||||
latest: Timestamp,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum InboxRow {
|
||||
Repo(usize),
|
||||
@@ -41,12 +34,18 @@ enum InboxRow {
|
||||
Empty,
|
||||
}
|
||||
|
||||
struct InboxSection {
|
||||
address: Option<RepoAddr>,
|
||||
unread: usize,
|
||||
entries: Vec<usize>,
|
||||
latest: Timestamp,
|
||||
}
|
||||
|
||||
pub struct InboxView {
|
||||
dock: WeakEntity<DockState>,
|
||||
threads: Arc<Vec<InboxItem>>,
|
||||
sections: Arc<Vec<InboxSection>>,
|
||||
rows: Arc<Vec<InboxRow>>,
|
||||
unread_count: usize,
|
||||
state: InboxReadState,
|
||||
state_loaded: bool,
|
||||
refresh: RefreshGate,
|
||||
@@ -89,7 +88,6 @@ impl InboxView {
|
||||
threads: Arc::new(Vec::new()),
|
||||
sections: Arc::new(Vec::new()),
|
||||
rows: Arc::new(Vec::new()),
|
||||
unread_count: 0,
|
||||
state: InboxReadState::default(),
|
||||
state_loaded: false,
|
||||
refresh: RefreshGate::default(),
|
||||
@@ -182,7 +180,7 @@ impl InboxView {
|
||||
let work = cx.background_spawn(async move { Inbox::query(&client, me, &state).await });
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
let (threads, unread_count) = match work.await {
|
||||
let (threads, _unread_count) = match work.await {
|
||||
Ok(results) => results,
|
||||
Err(error) => {
|
||||
log::warn!("inbox refresh failed: {error}");
|
||||
@@ -196,7 +194,6 @@ impl InboxView {
|
||||
return false;
|
||||
}
|
||||
this.threads = Arc::new(threads);
|
||||
this.unread_count = unread_count;
|
||||
this.rebuild(cx);
|
||||
cx.notify();
|
||||
this.refresh.finish()
|
||||
@@ -214,7 +211,6 @@ impl InboxView {
|
||||
for item in items.iter_mut() {
|
||||
item.apply_state(&self.state);
|
||||
}
|
||||
self.unread_count = items.iter().filter(|item| item.is_unread()).count();
|
||||
self.threads = Arc::new(items);
|
||||
self.rebuild(cx);
|
||||
}
|
||||
@@ -310,7 +306,6 @@ impl InboxView {
|
||||
self.threads = Arc::new(Vec::new());
|
||||
self.sections = Arc::new(Vec::new());
|
||||
self.rows = Arc::new(Vec::new());
|
||||
self.unread_count = 0;
|
||||
self.state = InboxReadState::default();
|
||||
self.state_loaded = false;
|
||||
self.refresh = RefreshGate::default();
|
||||
@@ -348,7 +343,15 @@ impl InboxView {
|
||||
return div().into_any_element();
|
||||
};
|
||||
let name = repo_name(section.address.as_ref(), cx);
|
||||
repo_header(name, section.unread)
|
||||
let id = SharedString::from(format!(
|
||||
"inbox-unread:{}",
|
||||
section
|
||||
.address
|
||||
.as_ref()
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_default()
|
||||
));
|
||||
repo_header(id, name, section.unread)
|
||||
}
|
||||
InboxRow::Entry(section_ix, entry_ix) => {
|
||||
let Some(section) = self.sections.get(section_ix) else {
|
||||
@@ -389,19 +392,6 @@ impl InboxView {
|
||||
}
|
||||
}
|
||||
|
||||
fn repo_name(addr: Option<&RepoAddr>, cx: &App) -> SharedString {
|
||||
let Some(addr) = addr else {
|
||||
return SharedString::from("Untitled");
|
||||
};
|
||||
RepoListStore::global(cx)
|
||||
.read(cx)
|
||||
.announcements
|
||||
.iter()
|
||||
.find(|announcement| announcement.addr() == *addr)
|
||||
.and_then(|announcement| announcement.name.as_deref().map(SharedString::from))
|
||||
.unwrap_or_else(|| SharedString::from("Untitled"))
|
||||
}
|
||||
|
||||
impl Panel for InboxView {
|
||||
fn title(&self, _cx: &App) -> Option<SharedString> {
|
||||
Some(SharedString::from("Inbox"))
|
||||
@@ -465,3 +455,22 @@ impl Render for InboxView {
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn repo_name(addr: Option<&RepoAddr>, cx: &App) -> SharedString {
|
||||
let Some(addr) = addr else {
|
||||
return SharedString::from("Untitled");
|
||||
};
|
||||
RepoListStore::global(cx)
|
||||
.read(cx)
|
||||
.announcements
|
||||
.iter()
|
||||
.find(|announcement| announcement.addr() == *addr)
|
||||
.map(|announcement| {
|
||||
announcement
|
||||
.name
|
||||
.as_deref()
|
||||
.map(SharedString::from)
|
||||
.unwrap_or_else(|| SharedString::from(announcement.id.clone()))
|
||||
})
|
||||
.unwrap_or_else(|| SharedString::from("Untitled"))
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ impl IssueDetailView {
|
||||
let directory = ProfileDirectory::new(cx);
|
||||
let weak = cx.entity().downgrade();
|
||||
cx.defer(move |cx| {
|
||||
let _ = weak.update(cx, |this, cx| this.track_participants(cx));
|
||||
crate::shared::log_dropped(weak.update(cx, |this, cx| this.track_participants(cx)));
|
||||
});
|
||||
|
||||
Self {
|
||||
|
||||
@@ -60,7 +60,7 @@ impl IssuesView {
|
||||
});
|
||||
let weak = cx.entity().downgrade();
|
||||
cx.defer(move |cx| {
|
||||
let _ = weak.update(cx, |this, cx| this.rebuild(cx));
|
||||
crate::shared::log_dropped(weak.update(cx, |this, cx| this.rebuild(cx)));
|
||||
});
|
||||
|
||||
Self {
|
||||
|
||||
@@ -83,7 +83,7 @@ impl Render for NewIssueDialog {
|
||||
Button::new("create-issue")
|
||||
.label("Create issue")
|
||||
.on_click(move |_, _, cx| {
|
||||
let _ = this.update(cx, |this, cx| this.submit(cx));
|
||||
crate::shared::log_dropped(this.update(cx, |this, cx| this.submit(cx)));
|
||||
})
|
||||
.into_any_element() as AnyElement,
|
||||
]
|
||||
|
||||
@@ -85,7 +85,7 @@ impl PullRequestDetailView {
|
||||
});
|
||||
|
||||
cx.defer(move |cx| {
|
||||
let _ = weak.update(cx, |this, cx| this.sync(cx));
|
||||
crate::shared::log_dropped(weak.update(cx, |this, cx| this.sync(cx)));
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -291,7 +291,6 @@ impl PullRequestDetailView {
|
||||
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();
|
||||
@@ -313,7 +312,6 @@ impl PullRequestDetailView {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let _ = dock_for_build;
|
||||
}
|
||||
|
||||
fn render_tabs(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
@@ -339,10 +337,10 @@ impl PullRequestDetailView {
|
||||
]
|
||||
})
|
||||
.on_change(move |index, _, cx| {
|
||||
let _ = weak.update(cx, |this, cx| {
|
||||
crate::shared::log_dropped(weak.update(cx, |this, cx| {
|
||||
this.active_tab = index;
|
||||
cx.notify();
|
||||
});
|
||||
}));
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -476,9 +474,9 @@ impl PullRequestDetailView {
|
||||
&commit,
|
||||
move |window, cx| {
|
||||
if let Some(worktree) = worktree.clone() {
|
||||
let _ = view.update(cx, |this, cx| {
|
||||
crate::shared::log_dropped(view.update(cx, |this, cx| {
|
||||
this.open_commit_diff(worktree, &id, window, cx);
|
||||
});
|
||||
}));
|
||||
}
|
||||
},
|
||||
cx,
|
||||
@@ -555,7 +553,7 @@ impl PullRequestDetailView {
|
||||
.when(can_update, |this| {
|
||||
this.child(Button::new("update-pr").label("Update").small().on_click(
|
||||
move |_, _, cx| {
|
||||
let _ = weak.update(cx, |this, cx| {
|
||||
crate::shared::log_dropped(weak.update(cx, |this, cx| {
|
||||
let root = this
|
||||
.store
|
||||
.read(cx)
|
||||
@@ -568,7 +566,7 @@ impl PullRequestDetailView {
|
||||
if let Some(root) = root {
|
||||
dialogs::update::open(this.store.clone(), root, cx);
|
||||
}
|
||||
});
|
||||
}));
|
||||
},
|
||||
))
|
||||
})
|
||||
|
||||
@@ -13,10 +13,6 @@ 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,
|
||||
@@ -76,10 +72,14 @@ impl Render for UpdatePrDialog {
|
||||
Button::new("submit-update")
|
||||
.label("Update pull request")
|
||||
.on_click(move |_, _, cx| {
|
||||
let _ = this.update(cx, |this, cx| this.submit(cx));
|
||||
crate::shared::log_dropped(this.update(cx, |this, cx| this.submit(cx)));
|
||||
})
|
||||
.into_any_element() as AnyElement,
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open(store: Entity<RepoStore>, root: Event, cx: &mut App) {
|
||||
open_global(UpdatePrDialog::new(store, root, cx), cx);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
use gpui::SharedString;
|
||||
use signed_core::RepoStatus;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use gpui::{SharedString, WeakEntity};
|
||||
use gpui_native_kit::menu::MenuItem;
|
||||
use nostr::prelude::PublicKey;
|
||||
use signed_core::{Announcement, RepoAddr, RepoStatus};
|
||||
use utils::middle_truncate;
|
||||
|
||||
use super::NewPullRequestView;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PullRequestFilter {
|
||||
@@ -32,20 +39,6 @@ impl PullRequestFilter {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -62,6 +55,14 @@ impl ForkCompare {
|
||||
}
|
||||
}
|
||||
|
||||
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}"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fork_display_name(announcement: &Announcement) -> SharedString {
|
||||
announcement
|
||||
.name
|
||||
@@ -77,3 +78,74 @@ pub fn shorten_owner(owner: &PublicKey) -> String {
|
||||
pub fn truncate_label(label: &str) -> SharedString {
|
||||
SharedString::from(middle_truncate(label, 18, 0))
|
||||
}
|
||||
|
||||
pub fn source_menu_items(
|
||||
view: WeakEntity<NewPullRequestView>,
|
||||
associated: Vec<PathBuf>,
|
||||
active_path: Option<PathBuf>,
|
||||
candidates: Vec<Announcement>,
|
||||
user: Option<PublicKey>,
|
||||
active_fork: Option<RepoAddr>,
|
||||
) -> Vec<MenuItem> {
|
||||
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 target = view.clone();
|
||||
let path_for_action = path.clone();
|
||||
items.push(
|
||||
MenuItem::new(truncate_label(&label))
|
||||
.checked(active_path.as_ref() == Some(&path))
|
||||
.handler(move |window, cx| {
|
||||
let path = path_for_action.clone();
|
||||
if let Err(error) =
|
||||
target.update(cx, |this, cx| this.apply_folder_path(path, window, cx))
|
||||
{
|
||||
log::warn!("failed to apply checkout: {error}");
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let folder_view = view.clone();
|
||||
items.push(
|
||||
MenuItem::new("Choose another folder…").handler(move |window, cx| {
|
||||
if let Err(error) = folder_view.update(cx, |this, cx| this.choose_checkout(window, cx))
|
||||
{
|
||||
log::warn!("failed to choose checkout: {error}");
|
||||
}
|
||||
}),
|
||||
);
|
||||
items.push(MenuItem::separator());
|
||||
|
||||
if candidates.is_empty() {
|
||||
items.push(MenuItem::new("No announced forks of this repository").disabled(true));
|
||||
} else {
|
||||
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 target = view.clone();
|
||||
let checked = active_fork == Some(candidate.addr());
|
||||
let announcement = candidate;
|
||||
items.push(
|
||||
MenuItem::new(truncate_label(&title))
|
||||
.checked(checked)
|
||||
.handler(move |window, cx| {
|
||||
let announcement = announcement.clone();
|
||||
if let Err(error) =
|
||||
target.update(cx, |this, cx| this.choose_fork(announcement, window, cx))
|
||||
{
|
||||
log::warn!("failed to choose fork: {error}");
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
items
|
||||
}
|
||||
|
||||
@@ -64,9 +64,9 @@ impl PullRequestsView {
|
||||
});
|
||||
|
||||
cx.defer(move |cx| {
|
||||
let _ = weak.update(cx, |this, cx| {
|
||||
crate::shared::log_dropped(weak.update(cx, |this, cx| {
|
||||
this.rebuild(cx);
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -255,18 +255,18 @@ impl PullRequestsView {
|
||||
}
|
||||
})
|
||||
.on_open_change(move |open, _, cx| {
|
||||
let _ = menu_open_weak.update(cx, |this, cx| {
|
||||
crate::shared::log_dropped(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| {
|
||||
crate::shared::log_dropped(weak.update(cx, |this, cx| {
|
||||
this.open_send_patch(window, cx);
|
||||
});
|
||||
}));
|
||||
}
|
||||
})]
|
||||
}),
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
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 _, PathPromptOptions, Render, SharedString, Styled as _, Subscription,
|
||||
UniformListScrollHandle, WeakEntity, Window, div, px, uniform_list,
|
||||
ParentElement as _, 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};
|
||||
@@ -14,45 +14,45 @@ 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::menu::Menu;
|
||||
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 signed_state::{CheckoutsStore, RepoStore};
|
||||
|
||||
use super::helpers::{ForkCompare, fork_display_name, shorten_owner, truncate_label};
|
||||
use super::helpers::{ForkCompare, fork_display_name, truncate_label};
|
||||
use crate::repo::commit_diff::{CommitDiffView, DiffPane};
|
||||
use crate::repo::helpers::commit_row;
|
||||
use crate::shared::widgets::empty_state;
|
||||
|
||||
mod source;
|
||||
|
||||
pub struct NewPullRequestView {
|
||||
dock: WeakEntity<DockState>,
|
||||
panel_id: SharedString,
|
||||
store: Entity<RepoStore>,
|
||||
pub(super) 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>>,
|
||||
pub(super) repo_path: Option<PathBuf>,
|
||||
pub(super) branches: Vec<SharedString>,
|
||||
pub(super) fork: Option<ForkCompare>,
|
||||
pub(super) base: SharedString,
|
||||
pub(super) compare: SharedString,
|
||||
pub(super) base_select: Entity<ComboboxState<SharedString>>,
|
||||
pub(super) 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>,
|
||||
pub(super) merge_base: Option<String>,
|
||||
pub(super) commits: Option<Vec<signed_git::FileCommit>>,
|
||||
pub(super) loading: bool,
|
||||
pub(super) error: Option<SharedString>,
|
||||
submitting: bool,
|
||||
compare_generation: u64,
|
||||
pub(super) compare_generation: u64,
|
||||
pub(super) fork_generation: u64,
|
||||
active_tab: usize,
|
||||
pane: Entity<DiffPane>,
|
||||
pub(super) pane: Entity<DiffPane>,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
tasks: Vec<gpui::Task<Result<(), anyhow::Error>>>,
|
||||
pub(super) tasks: Vec<gpui::Task<Result<(), anyhow::Error>>>,
|
||||
directory: Entity<ProfileDirectory>,
|
||||
source_open: bool,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
@@ -80,10 +80,13 @@ impl NewPullRequestView {
|
||||
let weak = weak.clone();
|
||||
move |value, _window, cx| {
|
||||
if let Some(base) = value {
|
||||
let _ = weak.update(cx, |this, cx| {
|
||||
crate::shared::log_dropped(weak.update(cx, |this, cx| {
|
||||
if this.base == base {
|
||||
return;
|
||||
}
|
||||
this.base = base;
|
||||
this.reload_compare(cx);
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -98,10 +101,13 @@ impl NewPullRequestView {
|
||||
let weak = weak.clone();
|
||||
move |value, _window, cx| {
|
||||
if let Some(compare) = value {
|
||||
let _ = weak.update(cx, |this, cx| {
|
||||
crate::shared::log_dropped(weak.update(cx, |this, cx| {
|
||||
if this.compare == compare {
|
||||
return;
|
||||
}
|
||||
this.compare = compare;
|
||||
this.reload_compare(cx);
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -115,7 +121,7 @@ impl NewPullRequestView {
|
||||
}
|
||||
|
||||
window.defer(cx, move |window, cx| {
|
||||
let _ = weak.update(cx, |this, cx| {
|
||||
crate::shared::log_dropped(weak.update(cx, |this, cx| {
|
||||
let Some(addr) = this.store.read(cx).addr().cloned() else {
|
||||
return;
|
||||
};
|
||||
@@ -130,7 +136,7 @@ impl NewPullRequestView {
|
||||
};
|
||||
|
||||
this.apply_folder_path(path, window, cx);
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -153,6 +159,7 @@ impl NewPullRequestView {
|
||||
error: None,
|
||||
submitting: false,
|
||||
compare_generation: 0,
|
||||
fork_generation: 0,
|
||||
active_tab: 0,
|
||||
pane,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
@@ -163,363 +170,6 @@ impl NewPullRequestView {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -566,7 +216,7 @@ impl NewPullRequestView {
|
||||
this.update(cx, |this, _cx| {
|
||||
this.submitting = false;
|
||||
})?;
|
||||
let _ = dock.update(cx, |dock, cx| dock.close_panel(&panel_id, cx));
|
||||
crate::shared::log_dropped(dock.update(cx, |dock, cx| dock.close_panel(&panel_id, cx)));
|
||||
Ok(())
|
||||
});
|
||||
self.tasks.push(task);
|
||||
@@ -619,7 +269,19 @@ impl NewPullRequestView {
|
||||
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 source_view = cx.entity().downgrade();
|
||||
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 candidates = self.fork_candidates(cx);
|
||||
let user = signed_state::Backend::global(cx).read(cx).current_user();
|
||||
let active_fork = self.fork.as_ref().map(|fork| fork.announcement.addr());
|
||||
let can_submit = self.submit_enabled(cx);
|
||||
let source_label = match &self.fork {
|
||||
Some(fork) => truncate_label(&fork_display_name(&fork.announcement)),
|
||||
@@ -653,10 +315,10 @@ impl NewPullRequestView {
|
||||
Menu::new("pr-source")
|
||||
.open(self.source_open)
|
||||
.on_change(move |open, _, cx| {
|
||||
let _ = weak_menu.update(cx, |this, cx| {
|
||||
crate::shared::log_dropped(weak_menu.update(cx, |this, cx| {
|
||||
this.source_open = open;
|
||||
cx.notify();
|
||||
});
|
||||
}));
|
||||
})
|
||||
.trigger(move |_, _| {
|
||||
Button::new("compare-source")
|
||||
@@ -664,7 +326,16 @@ impl NewPullRequestView {
|
||||
.variant(gpui_native_kit::button::ButtonVariant::Secondary)
|
||||
.into_any_element()
|
||||
})
|
||||
.items(move |_, _| source_items.clone()),
|
||||
.items(move |_, _| {
|
||||
super::helpers::source_menu_items(
|
||||
source_view.clone(),
|
||||
associated.clone(),
|
||||
active_path.clone(),
|
||||
candidates.clone(),
|
||||
user,
|
||||
active_fork.clone(),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.when_some(refresh_fork, |this, fork| {
|
||||
@@ -684,76 +355,14 @@ impl NewPullRequestView {
|
||||
.loading(self.submitting)
|
||||
.disabled(!can_submit)
|
||||
.on_click(move |_, window, cx| {
|
||||
let _ = weak.update(cx, |this, cx| this.submit(window, cx));
|
||||
crate::shared::log_dropped(
|
||||
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());
|
||||
@@ -770,10 +379,10 @@ impl NewPullRequestView {
|
||||
]
|
||||
})
|
||||
.on_change(move |index, _, cx| {
|
||||
let _ = weak.update(cx, |this, cx| {
|
||||
crate::shared::log_dropped(weak.update(cx, |this, cx| {
|
||||
this.active_tab = index;
|
||||
cx.notify();
|
||||
});
|
||||
}));
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -824,9 +433,9 @@ impl NewPullRequestView {
|
||||
ix,
|
||||
&commit,
|
||||
move |window, cx| {
|
||||
let _ = view.update(cx, |this, cx| {
|
||||
crate::shared::log_dropped(view.update(cx, |this, cx| {
|
||||
this.open_commit_diff(&id, window, cx);
|
||||
});
|
||||
}));
|
||||
},
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use gpui::{App, AppContext as _, Context, PathPromptOptions, SharedString, Window};
|
||||
use nostr::prelude::Url;
|
||||
use signed_core::Announcement;
|
||||
use signed_git::{GitCache, Repo};
|
||||
use signed_state::{Backend, CheckoutsStore, Mirrors, RepoListStore};
|
||||
|
||||
use super::NewPullRequestView;
|
||||
use crate::pull_requests::helpers::ForkCompare;
|
||||
|
||||
impl NewPullRequestView {
|
||||
pub(super) fn has_source(&self) -> bool {
|
||||
self.repo_path.is_some() || self.fork.is_some()
|
||||
}
|
||||
|
||||
pub(super) fn work_path(&self) -> Option<PathBuf> {
|
||||
match &self.fork {
|
||||
Some(fork) => Some(fork.mirror_path.clone()),
|
||||
None => self.repo_path.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn base_ref(&self) -> String {
|
||||
match &self.fork {
|
||||
Some(_) => ForkCompare::base_ref(&self.base),
|
||||
None => self.base.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn compare_ref(&self) -> String {
|
||||
match &self.fork {
|
||||
Some(fork) => fork.compare_ref(&self.compare),
|
||||
None => self.compare.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) 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);
|
||||
}
|
||||
|
||||
pub(crate) fn apply_folder_path(
|
||||
&mut self,
|
||||
path: PathBuf,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.fork_generation = self.fork_generation.wrapping_add(1);
|
||||
let generation = self.fork_generation;
|
||||
let path_string = path.to_string_lossy().to_string();
|
||||
let task: gpui::Task<Result<(), anyhow::Error>> =
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let info = cx
|
||||
.background_spawn({
|
||||
let path = path.clone();
|
||||
async move {
|
||||
let repo = Repo::try_open(&path)?;
|
||||
match repo.branches() {
|
||||
Ok(branches) => Some((branches, repo.current_branch())),
|
||||
Err(error) => {
|
||||
log::warn!("failed to list branches: {error}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
if generation != this.fork_generation {
|
||||
return;
|
||||
}
|
||||
this.apply_checkout(path_string, info, window, cx);
|
||||
})?;
|
||||
Ok(())
|
||||
});
|
||||
self.tasks.push(task);
|
||||
}
|
||||
|
||||
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();
|
||||
self.merge_base = None;
|
||||
self.commits = None;
|
||||
self.pane.update(cx, |pane, cx| pane.clear(cx));
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) 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()
|
||||
}
|
||||
|
||||
pub(crate) 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();
|
||||
self.fork_generation = self.fork_generation.wrapping_add(1);
|
||||
let generation = self.fork_generation;
|
||||
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| {
|
||||
if generation != this.fork_generation {
|
||||
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);
|
||||
}
|
||||
|
||||
pub(super) 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);
|
||||
}
|
||||
}
|
||||
@@ -102,9 +102,10 @@ impl SendPatchView {
|
||||
}
|
||||
|
||||
let panel_id = self.panel_id.clone();
|
||||
let _ = self
|
||||
.dock
|
||||
.update(cx, |dock, cx| dock.close_panel(&panel_id, cx));
|
||||
crate::shared::log_dropped(
|
||||
self.dock
|
||||
.update(cx, |dock, cx| dock.close_panel(&panel_id, cx)),
|
||||
);
|
||||
}
|
||||
|
||||
fn render_footer(&self, cx: &Context<Self>) -> AnyElement {
|
||||
@@ -127,7 +128,7 @@ impl SendPatchView {
|
||||
.loading(self.submitting)
|
||||
.disabled(!can_submit)
|
||||
.on_click(move |_, _, cx| {
|
||||
let _ = weak.update(cx, |this, cx| this.submit(cx));
|
||||
crate::shared::log_dropped(weak.update(cx, |this, cx| this.submit(cx)));
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
|
||||
@@ -7,6 +7,13 @@ use signed_state::RepoStore;
|
||||
use crate::repo::RepoDetailView;
|
||||
use crate::shared::panels;
|
||||
|
||||
/// An item of a repository to open from outside its detail panel.
|
||||
pub enum RepoItem {
|
||||
Issue(EventId),
|
||||
PullRequest(EventId),
|
||||
Patch,
|
||||
}
|
||||
|
||||
/// Opens a repository as a dock panel, keyed by its address.
|
||||
pub fn open_repo_panel(
|
||||
dock: &WeakEntity<DockState>,
|
||||
@@ -33,13 +40,6 @@ pub fn open_repo_panel(
|
||||
);
|
||||
}
|
||||
|
||||
/// An item of a repository to open from outside its detail panel.
|
||||
pub enum RepoItem {
|
||||
Issue(EventId),
|
||||
PullRequest(EventId),
|
||||
Patch,
|
||||
}
|
||||
|
||||
/// Opens an issue or pull request using a store built here.
|
||||
pub fn open_repo_item(
|
||||
dock: &WeakEntity<DockState>,
|
||||
|
||||
@@ -222,7 +222,9 @@ impl Render for CreateRepoDialog {
|
||||
.loading(busy)
|
||||
.disabled(busy)
|
||||
.on_click(move |_, window, cx| {
|
||||
let _ = this.update(cx, |this, cx| this.create(window, cx));
|
||||
crate::shared::log_dropped(
|
||||
this.update(cx, |this, cx| this.create(window, cx)),
|
||||
);
|
||||
})
|
||||
.into_any_element();
|
||||
|
||||
|
||||
@@ -243,7 +243,9 @@ impl Render for InitRepoDialog {
|
||||
.loading(busy)
|
||||
.disabled(busy)
|
||||
.on_click(move |_, window, cx| {
|
||||
let _ = this.update(cx, |this, cx| this.init_repository(window, cx));
|
||||
crate::shared::log_dropped(
|
||||
this.update(cx, |this, cx| this.init_repository(window, cx)),
|
||||
);
|
||||
})
|
||||
.into_any_element();
|
||||
|
||||
|
||||
@@ -69,8 +69,7 @@ pub struct RepoFilesView {
|
||||
}
|
||||
|
||||
impl RepoFilesView {
|
||||
pub fn new(cx: &mut Context<Self>) -> Self {
|
||||
let _ = cx;
|
||||
pub fn new(_cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
seeds: Vec::new(),
|
||||
expanded: HashSet::new(),
|
||||
@@ -448,13 +447,7 @@ impl RepoFilesView {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn set_code(
|
||||
&mut self,
|
||||
path: SharedString,
|
||||
text: &str,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
fn set_code(&mut self, path: SharedString, text: &str, cx: &mut Context<Self>) {
|
||||
let hash = source_hash(text);
|
||||
let language = code_language(path.as_ref());
|
||||
|
||||
@@ -480,7 +473,6 @@ impl RepoFilesView {
|
||||
}
|
||||
|
||||
let state = cx.new(|cx| CodeViewState::new("repo-code", text, language, cx));
|
||||
let _ = window;
|
||||
self.code = Some(CodeView {
|
||||
path,
|
||||
state,
|
||||
@@ -514,7 +506,7 @@ impl RepoFilesView {
|
||||
self.set_markdown(Some(path.into()), &text, cx);
|
||||
}
|
||||
} else if self.code.as_ref().map(|code| code.path.as_str()) != Some(path) {
|
||||
self.set_code(path.into(), &text, window, cx);
|
||||
self.set_code(path.into(), &text, cx);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
@@ -574,7 +566,7 @@ impl RepoFilesView {
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.update_in(cx, |this, _window, cx| {
|
||||
this.loading_files.remove(&path);
|
||||
|
||||
match content {
|
||||
@@ -590,7 +582,7 @@ impl RepoFilesView {
|
||||
let same = this.code.as_ref().map(|code| code.path.as_str())
|
||||
== Some(path.as_str());
|
||||
if !same {
|
||||
this.set_code(path.clone().into(), text, window, cx);
|
||||
this.set_code(path.clone().into(), text, cx);
|
||||
}
|
||||
}
|
||||
this.preview_bytes += text.len();
|
||||
|
||||
@@ -0,0 +1,667 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use assets::CustomIconName;
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AnyElement, AppContext as _, ClipboardItem, Context, IntoElement, ParentElement as _,
|
||||
SharedString, Styled as _, Window, div, px,
|
||||
};
|
||||
use gpui_native_kit::avatar::Avatar;
|
||||
use gpui_native_kit::button::{Button, ButtonVariant};
|
||||
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::dropdown_menu::DropdownMenu;
|
||||
use gpui_native_kit::icons::{Icon, IconRole};
|
||||
use gpui_native_kit::menu::MenuItem;
|
||||
use gpui_native_kit::popover::Popover;
|
||||
use gpui_native_kit::theme::ActiveTheme as _;
|
||||
use gpui_native_kit::tooltip::Tooltip;
|
||||
use signed_state::{Backend, Nip34Kind};
|
||||
|
||||
use super::RepoDetailView;
|
||||
use super::actions;
|
||||
use super::dialogs;
|
||||
use super::helpers::{
|
||||
ShareTargets, bound_repo_label, clone_group, fork_row, nostr_clone_url, repo_display_name,
|
||||
};
|
||||
use crate::issues::IssuesView;
|
||||
use crate::pull_requests::{NewPullRequestView, PullRequestsView, SendPatchView};
|
||||
|
||||
impl RepoDetailView {
|
||||
pub(super) fn open_upstream(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(addr) = self
|
||||
.announcement(cx)
|
||||
.and_then(|announcement| announcement.upstream.as_ref())
|
||||
.and_then(|upstream| upstream.addr.clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
actions::open_repo_panel(&self.dock, &addr, None, window, cx);
|
||||
}
|
||||
|
||||
/// Open the dialog that publishes the local repository to NIP-34.
|
||||
fn open_init_dialog(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(local_path) = self.store.read(cx).path.clone() else {
|
||||
return;
|
||||
};
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) 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>) {
|
||||
let Some(addr) = self.store.read(cx).addr().cloned() else {
|
||||
return;
|
||||
};
|
||||
let id = SharedString::from(format!("issues:{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| IssuesView::new(dock_for_build.clone(), store.clone(), cx)),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn render_header(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||
if self.store.read(cx).addr().is_none() {
|
||||
return self.render_local_header(cx);
|
||||
}
|
||||
|
||||
let store = self.store.read(cx);
|
||||
let pushing = store.pushing;
|
||||
let cloning = store.cloning;
|
||||
|
||||
let Some(announcement) = store.announcement.clone() else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
let nip05 = self
|
||||
.directory
|
||||
.read(cx)
|
||||
.profile(&announcement.owner, cx)
|
||||
.metadata()
|
||||
.nip05
|
||||
.clone()
|
||||
.filter(|nip05| !nip05.trim().is_empty());
|
||||
|
||||
let share = ShareTargets::from_announcement(&announcement);
|
||||
let nostr_url = nostr_clone_url(&announcement, nip05.as_deref());
|
||||
let ngit_command = SharedString::from(format!("git clone {nostr_url}"));
|
||||
let nak_command = SharedString::from(format!("nak git clone {nostr_url}"));
|
||||
let git_commands: Vec<String> = announcement.clone_urls();
|
||||
|
||||
let name = repo_display_name(self.store.read(cx));
|
||||
let description = announcement.description();
|
||||
let avatar = Avatar::new(format!("{}:{}", announcement.owner, announcement.id));
|
||||
|
||||
let share_naddr = share.naddr.clone();
|
||||
let share_for_menu = share.clone();
|
||||
let announcement_for_menu = announcement.clone();
|
||||
let weak_share = cx.entity().downgrade();
|
||||
let weak_menu_open = cx.entity().downgrade();
|
||||
let weak_menu_items = cx.entity().downgrade();
|
||||
let weak_clone = cx.entity().downgrade();
|
||||
let weak_download = cx.entity().downgrade();
|
||||
let ngit_for_popover = ngit_command.clone();
|
||||
let nak_for_popover = nak_command.clone();
|
||||
let git_for_popover = git_commands.clone();
|
||||
|
||||
v_flex()
|
||||
.flex_none()
|
||||
.p_4()
|
||||
.w_full()
|
||||
.gap_6()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors.border)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_4()
|
||||
.items_start()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||
.child(avatar)
|
||||
.child(name),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().colors.muted_foreground)
|
||||
.line_clamp(2)
|
||||
.child(description),
|
||||
)
|
||||
.when_some(fork_row(&announcement, cx), |this, row| this.child(row))
|
||||
.child(
|
||||
h_flex()
|
||||
.mt_2()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().colors.muted_foreground)
|
||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||
.child("Maintainers:"),
|
||||
)
|
||||
.child(self.render_maintainers(cx)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.gap_2()
|
||||
.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| {
|
||||
crate::shared::log_dropped(weak_new.update(cx, |this, cx| {
|
||||
this.open_new_pull_request(window, cx);
|
||||
}));
|
||||
}),
|
||||
MenuItem::new("Send patch").handler(move |window, cx| {
|
||||
crate::shared::log_dropped(weak_patch.update(cx, |this, cx| {
|
||||
this.open_send_patch(window, cx);
|
||||
}));
|
||||
}),
|
||||
]
|
||||
}
|
||||
})
|
||||
})
|
||||
.child({
|
||||
let weak_issues = cx.entity().downgrade();
|
||||
let weak_issues_open = cx.entity().downgrade();
|
||||
let store_for_issues = self.store.clone();
|
||||
let issues_count = self.store.read(cx).issue_count();
|
||||
|
||||
DropdownMenu::new("issues")
|
||||
.label(format!("Issues {issues_count}"))
|
||||
.icon(Icon::named(CustomIconName::GitIssueDone))
|
||||
.variant(ButtonVariant::Secondary)
|
||||
.open(self.issues_menu_open)
|
||||
.on_click(move |_, window, cx| {
|
||||
if let Err(error) = weak_issues.update(cx, |this, cx| {
|
||||
this.open_issues(window, cx);
|
||||
}) {
|
||||
log::warn!("failed to open issues: {error}");
|
||||
}
|
||||
})
|
||||
.on_open_change(move |open, _, cx| {
|
||||
if let Err(error) =
|
||||
weak_issues_open.update(cx, |this, cx| {
|
||||
this.issues_menu_open = open;
|
||||
cx.notify();
|
||||
})
|
||||
{
|
||||
log::warn!("failed to update issues menu: {error}");
|
||||
}
|
||||
})
|
||||
.items(move |_, _| {
|
||||
vec![MenuItem::new("New issue").handler({
|
||||
let store = store_for_issues.clone();
|
||||
move |_, cx| {
|
||||
crate::issues::new_issue::open(store.clone(), cx);
|
||||
}
|
||||
})]
|
||||
})
|
||||
})
|
||||
.child(
|
||||
DropdownMenu::new("share")
|
||||
.icon(Icon::named(IconRole::Copy))
|
||||
.variant(ButtonVariant::Secondary)
|
||||
.open(self.share_open)
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
share_naddr.clone(),
|
||||
));
|
||||
})
|
||||
.on_open_change(move |open, _, cx| {
|
||||
if let Err(error) = weak_share.update(cx, |this, cx| {
|
||||
this.share_open = open;
|
||||
cx.notify();
|
||||
}) {
|
||||
log::warn!("failed to update share menu: {error}");
|
||||
}
|
||||
})
|
||||
.items(move |_, _| share_for_menu.menu()),
|
||||
)
|
||||
.child(
|
||||
DropdownMenu::new("repo-menu")
|
||||
.icon(Icon::named(CustomIconName::EllipsisVertical))
|
||||
.variant(ButtonVariant::Secondary)
|
||||
.loading(pushing)
|
||||
.disabled(pushing)
|
||||
.open(self.repo_menu_open)
|
||||
.on_open_change(move |open, _, cx| {
|
||||
if let Err(error) = weak_menu_open.update(cx, |this, cx| {
|
||||
this.repo_menu_open = open;
|
||||
cx.notify();
|
||||
}) {
|
||||
log::warn!("failed to update repo menu: {error}");
|
||||
}
|
||||
})
|
||||
.items(move |_, cx| {
|
||||
let owner = Backend::global(cx)
|
||||
.read(cx)
|
||||
.current_user()
|
||||
== Some(announcement_for_menu.owner);
|
||||
|
||||
let mut items = vec![MenuItem::new("About").handler({
|
||||
let weak = weak_menu_items.clone();
|
||||
let announcement = announcement_for_menu.clone();
|
||||
move |_, cx| {
|
||||
if let Err(error) = weak.update(cx, |this, cx| {
|
||||
dialogs::about::open(
|
||||
announcement.clone(),
|
||||
this.directory.clone(),
|
||||
cx,
|
||||
);
|
||||
}) {
|
||||
log::warn!("failed to open about: {error}");
|
||||
}
|
||||
}
|
||||
})];
|
||||
|
||||
if owner {
|
||||
items.push(MenuItem::new("Republish").handler({
|
||||
let weak = weak_menu_items.clone();
|
||||
move |window, cx| {
|
||||
if let Err(error) = weak.update(cx, |this, cx| {
|
||||
this.push_repository(window, cx);
|
||||
}) {
|
||||
log::warn!("failed to republish: {error}");
|
||||
}
|
||||
}
|
||||
}));
|
||||
items.push(MenuItem::new("Reset remote to local").handler({
|
||||
let weak = weak_menu_items.clone();
|
||||
move |window, cx| {
|
||||
if let Err(error) = weak.update(cx, |this, cx| {
|
||||
this.reset_remote(window, cx);
|
||||
}) {
|
||||
log::warn!("failed to reset remote: {error}");
|
||||
}
|
||||
}
|
||||
}));
|
||||
items.push(MenuItem::separator());
|
||||
items.push(
|
||||
MenuItem::new("Delete")
|
||||
.destructive(true)
|
||||
.handler({
|
||||
let weak = weak_menu_items.clone();
|
||||
move |window, cx| {
|
||||
if let Err(error) =
|
||||
weak.update(cx, |this, cx| {
|
||||
this.delete_repository(window, cx);
|
||||
})
|
||||
{
|
||||
log::warn!(
|
||||
"failed to delete repository: {error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
items
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Popover::new("clone")
|
||||
.open(self.clone_open)
|
||||
.on_change(move |open, _, cx| {
|
||||
if let Err(error) = weak_clone.update(cx, |this, cx| {
|
||||
this.clone_open = open;
|
||||
cx.notify();
|
||||
}) {
|
||||
log::warn!("failed to update clone popover: {error}");
|
||||
}
|
||||
})
|
||||
.trigger(move |_, _| {
|
||||
Button::new("clone")
|
||||
.icon(Icon::named(CustomIconName::GitClone))
|
||||
.tooltip(Tooltip::text("Clone"))
|
||||
.loading(cloning)
|
||||
.disabled(cloning)
|
||||
.into_any_element()
|
||||
})
|
||||
.content(move |_, cx| {
|
||||
v_flex()
|
||||
.w(px(440.))
|
||||
.p_3()
|
||||
.gap_4()
|
||||
.child(clone_group(
|
||||
"Clone with ngit",
|
||||
"copy-ngit",
|
||||
&ngit_for_popover,
|
||||
))
|
||||
.child(clone_group(
|
||||
"Clone with nak",
|
||||
"copy-nak",
|
||||
&nak_for_popover,
|
||||
))
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(
|
||||
cx.theme().colors.muted_foreground,
|
||||
)
|
||||
.child("Grasp Servers"),
|
||||
)
|
||||
.when(git_for_popover.is_empty(), |this| {
|
||||
this.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.child("No git clone urls."),
|
||||
)
|
||||
})
|
||||
.children(git_for_popover.iter().enumerate().map(
|
||||
|(ix, command)| {
|
||||
ClipboardButton::new(
|
||||
SharedString::from(format!(
|
||||
"copy-git-{ix}"
|
||||
)),
|
||||
command.clone(),
|
||||
)
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
h_flex().justify_end().child(
|
||||
Button::new("download")
|
||||
.icon(Icon::named(CustomIconName::GitClone))
|
||||
.label("Download")
|
||||
.on_click({
|
||||
let weak = weak_download.clone();
|
||||
move |_, window, cx| {
|
||||
if let Err(error) =
|
||||
weak.update(cx, |this, cx| {
|
||||
this.clone_to_folder(window, cx);
|
||||
})
|
||||
{
|
||||
log::warn!(
|
||||
"failed to clone: {error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(self.render_tabs(cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_local_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let name = repo_display_name(self.store.read(cx));
|
||||
let path = self
|
||||
.store
|
||||
.read(cx)
|
||||
.path
|
||||
.as_ref()
|
||||
.map(|path| path.display().to_string())
|
||||
.unwrap_or_default();
|
||||
let avatar = Avatar::new(path.clone());
|
||||
|
||||
let bound = self.store.read(cx).nip34.clone();
|
||||
let action = match bound
|
||||
.as_ref()
|
||||
.filter(|binding| binding.kind == Nip34Kind::Initialized)
|
||||
{
|
||||
Some(binding) => bound_repo_label(binding, &self.directory, cx),
|
||||
None => Button::new("init")
|
||||
.icon(Icon::named(CustomIconName::Init))
|
||||
.label("Initialize on Nostr")
|
||||
.tooltip(Tooltip::text("Publish this repository to Nostr"))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.open_init_dialog(window, cx);
|
||||
}))
|
||||
.into_any_element(),
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.flex_none()
|
||||
.px_4()
|
||||
.pb_4()
|
||||
.w_full()
|
||||
.gap_6()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors.border)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_4()
|
||||
.items_start()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||
.child(avatar)
|
||||
.child(name),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().colors.muted_foreground)
|
||||
.child(path),
|
||||
),
|
||||
)
|
||||
.child(action),
|
||||
)
|
||||
.child(self.render_tabs(cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_tabs(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let count = self.history.read(cx).commit_count();
|
||||
let muted = cx.theme().colors.muted_foreground;
|
||||
let weak = cx.entity().downgrade();
|
||||
let commits_label = match count {
|
||||
Some(count) => SharedString::from(format!("Commits ({count})")),
|
||||
None => SharedString::from("Commits"),
|
||||
};
|
||||
|
||||
Tabs::new("repo-tabs")
|
||||
.selected(self.active_tab)
|
||||
.tabs(move |_, _| {
|
||||
vec![
|
||||
Tab::new("files", "Files")
|
||||
.lead(Icon::named(CustomIconName::GitFile).color(muted)),
|
||||
Tab::new("commits", commits_label.clone())
|
||||
.lead(Icon::named(CustomIconName::GitCommit).color(muted)),
|
||||
]
|
||||
})
|
||||
.on_change(move |index, _, cx| {
|
||||
if let Err(error) = weak.update(cx, |this, cx| {
|
||||
this.active_tab = index;
|
||||
cx.notify();
|
||||
}) {
|
||||
log::warn!("failed to switch repo tab: {error}");
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_maintainers(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let Some(announcement) = self.announcement(cx) else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
let rest: Vec<_> = announcement
|
||||
.maintainers
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|key| key != &announcement.owner && seen.insert(*key))
|
||||
.collect();
|
||||
|
||||
let owner = self.directory.read(cx).profile(&announcement.owner, cx);
|
||||
let owner_name = owner.name();
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_3()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Avatar::new(announcement.owner)
|
||||
.name(owner_name.clone())
|
||||
.picture(owner.picture())
|
||||
.small(),
|
||||
)
|
||||
.child(div().text_xs().whitespace_nowrap().child(owner_name)),
|
||||
)
|
||||
.when(!rest.is_empty(), |this| {
|
||||
this.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().colors.muted_foreground)
|
||||
.child(SharedString::from(format!("+{}", rest.len()))),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,24 @@ use utils::{middle_truncate, relative_time_secs};
|
||||
use super::RepoDetailView;
|
||||
use crate::shared::tree::{TreeItemSeed, build_tree_items};
|
||||
|
||||
const GUTTER_WIDTH: f32 = 44.;
|
||||
pub const DIFF_ROW_HEIGHT: f32 = 20.;
|
||||
pub const COMMIT_ROW_HEIGHT: f32 = 56.;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) enum DiffRow {
|
||||
Hunk {
|
||||
old_start: u32,
|
||||
old_lines: u32,
|
||||
new_start: u32,
|
||||
new_lines: u32,
|
||||
},
|
||||
Line {
|
||||
hunk: usize,
|
||||
line: usize,
|
||||
},
|
||||
}
|
||||
|
||||
pub(super) struct RepoData {
|
||||
pub(super) tree: Vec<TreeItemSeed>,
|
||||
pub(super) entries: Vec<PathBuf>,
|
||||
@@ -38,6 +56,67 @@ pub(super) struct RepoData {
|
||||
pub(super) head_commit: Option<FileCommit>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct ShareTargets {
|
||||
/// NIP-19 `naddr1...` of the announcement, with its announced relays.
|
||||
pub(super) naddr: String,
|
||||
/// Hex ID of the announcement event itself.
|
||||
event_id: String,
|
||||
/// NIP-34 coordinate `30617:<pubkey>:<repo-id>`.
|
||||
coordinate: String,
|
||||
gitworkshop: String,
|
||||
ditto: String,
|
||||
}
|
||||
|
||||
impl ShareTargets {
|
||||
pub(super) fn from_announcement(announcement: &Announcement) -> Self {
|
||||
let addr = announcement.addr();
|
||||
let coordinate = addr.to_string();
|
||||
let naddr = Nip19Coordinate::new(addr.into(), announcement.relays.iter().cloned())
|
||||
.to_bech32()
|
||||
.unwrap_or_else(|_| coordinate.clone());
|
||||
|
||||
Self {
|
||||
naddr: naddr.clone(),
|
||||
event_id: announcement
|
||||
.event_id
|
||||
.to_bech32()
|
||||
.unwrap_or_else(|_| announcement.event_id.to_string()),
|
||||
coordinate,
|
||||
gitworkshop: format!("https://gitworkshop.dev/{naddr}"),
|
||||
ditto: format!("https://ditto.pub/{naddr}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn menu(&self) -> Vec<MenuItem> {
|
||||
let items = [
|
||||
("GitWorkshop", truncate_naddr_link(&self.gitworkshop, 4)),
|
||||
("Ditto", truncate_naddr_link(&self.ditto, 4)),
|
||||
("Event ID", middle_truncate(&self.event_id, 10, 10)),
|
||||
("Coordinate", middle_truncate(&self.coordinate, 10, 10)),
|
||||
];
|
||||
|
||||
let values = [
|
||||
self.gitworkshop.clone(),
|
||||
self.ditto.clone(),
|
||||
self.event_id.clone(),
|
||||
self.coordinate.clone(),
|
||||
];
|
||||
|
||||
items
|
||||
.into_iter()
|
||||
.zip(values)
|
||||
.map(|((label, detail), value)| {
|
||||
MenuItem::new(SharedString::from(format!("{label} {detail}"))).handler(
|
||||
move |_, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(value.clone()));
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn clone_group(label: &str, id: &'static str, command: &SharedString) -> AnyElement {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
@@ -106,67 +185,6 @@ pub(super) fn fork_row(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct ShareTargets {
|
||||
/// NIP-19 `naddr1...` of the announcement, with its announced relays.
|
||||
pub(super) naddr: String,
|
||||
/// Hex ID of the announcement event itself.
|
||||
event_id: String,
|
||||
/// NIP-34 coordinate `30617:<pubkey>:<repo-id>`.
|
||||
coordinate: String,
|
||||
gitworkshop: String,
|
||||
ditto: String,
|
||||
}
|
||||
|
||||
impl ShareTargets {
|
||||
pub(super) fn from_announcement(announcement: &Announcement) -> Self {
|
||||
let addr = announcement.addr();
|
||||
let coordinate = addr.to_string();
|
||||
let naddr = Nip19Coordinate::new(addr.into(), announcement.relays.iter().cloned())
|
||||
.to_bech32()
|
||||
.unwrap_or_else(|_| coordinate.clone());
|
||||
|
||||
Self {
|
||||
naddr: naddr.clone(),
|
||||
event_id: announcement
|
||||
.event_id
|
||||
.to_bech32()
|
||||
.unwrap_or_else(|_| announcement.event_id.to_string()),
|
||||
coordinate,
|
||||
gitworkshop: format!("https://gitworkshop.dev/{naddr}"),
|
||||
ditto: format!("https://ditto.pub/{naddr}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn menu(&self) -> Vec<MenuItem> {
|
||||
let items = [
|
||||
("GitWorkshop", truncate_naddr_link(&self.gitworkshop, 4)),
|
||||
("Ditto", truncate_naddr_link(&self.ditto, 4)),
|
||||
("Event ID", middle_truncate(&self.event_id, 10, 10)),
|
||||
("Coordinate", middle_truncate(&self.coordinate, 10, 10)),
|
||||
];
|
||||
|
||||
let values = [
|
||||
self.gitworkshop.clone(),
|
||||
self.ditto.clone(),
|
||||
self.event_id.clone(),
|
||||
self.coordinate.clone(),
|
||||
];
|
||||
|
||||
items
|
||||
.into_iter()
|
||||
.zip(values)
|
||||
.map(|((label, detail), value)| {
|
||||
MenuItem::new(SharedString::from(format!("{label} {detail}"))).handler(
|
||||
move |_, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(value.clone()));
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn truncate_naddr_link(url: &str, tail: usize) -> String {
|
||||
let Some(end) = url.find("naddr1").map(|i| i + "naddr1".len()) else {
|
||||
return url.to_string();
|
||||
@@ -314,24 +332,6 @@ pub(super) fn is_markdown_path(path: &str) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
const GUTTER_WIDTH: f32 = 44.;
|
||||
pub const DIFF_ROW_HEIGHT: f32 = 20.;
|
||||
pub const COMMIT_ROW_HEIGHT: f32 = 56.;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) enum DiffRow {
|
||||
Hunk {
|
||||
old_start: u32,
|
||||
old_lines: u32,
|
||||
new_start: u32,
|
||||
new_lines: u32,
|
||||
},
|
||||
Line {
|
||||
hunk: usize,
|
||||
line: usize,
|
||||
},
|
||||
}
|
||||
|
||||
pub(super) fn diff_rows(file: &FileDiff) -> Vec<DiffRow> {
|
||||
let mut rows = Vec::new();
|
||||
for (hunk_ix, hunk) in file.hunks.iter().enumerate() {
|
||||
|
||||
@@ -5,32 +5,25 @@ use anyhow::Error;
|
||||
use assets::CustomIconName;
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext as _, ClipboardItem, Context, Entity, InteractiveElement as _,
|
||||
IntoElement, ParentElement as _, PathPromptOptions, Render, SharedString, Styled as _,
|
||||
Subscription, Task, WeakEntity, Window, div, px,
|
||||
AnyElement, App, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement,
|
||||
ParentElement as _, PathPromptOptions, Render, SharedString, Styled as _, Subscription, Task,
|
||||
WeakEntity, Window, div, px,
|
||||
};
|
||||
use gpui_native_kit::alert::{Alert, AlertSeverity};
|
||||
use gpui_native_kit::avatar::Avatar;
|
||||
use gpui_native_kit::badge::Badge;
|
||||
use gpui_native_kit::button::{Button, ButtonVariant};
|
||||
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::dropdown_menu::DropdownMenu;
|
||||
use gpui_native_kit::icons::{Icon, IconRole};
|
||||
use gpui_native_kit::layout::dock::{DockState, Panel};
|
||||
use gpui_native_kit::menu::MenuItem;
|
||||
use gpui_native_kit::popover::Popover;
|
||||
use gpui_native_kit::spinner::Spinner;
|
||||
use gpui_native_kit::theme::ActiveTheme as _;
|
||||
use gpui_native_kit::tooltip::Tooltip;
|
||||
use metadata::ProfileDirectory;
|
||||
use nostr::prelude::{PublicKey, Url};
|
||||
use signed_core::{Announcement, RepoAddr};
|
||||
use signed_git::{FileCommit, GitCache, Repo};
|
||||
use signed_state::{
|
||||
Backend, CheckoutStatus, CheckoutSyncStatus, CheckoutsStore, LocalReposStore, Mirrors,
|
||||
Nip34Binding, Nip34Kind, PushStepStatus, RepoStore, SyncStatusDirectory,
|
||||
Nip34Binding, PushStepStatus, RepoStore, SyncStatusDirectory,
|
||||
};
|
||||
|
||||
pub mod actions;
|
||||
@@ -38,21 +31,17 @@ pub mod banners;
|
||||
pub mod commit_diff;
|
||||
pub mod dialogs;
|
||||
pub mod files;
|
||||
mod header;
|
||||
pub mod helpers;
|
||||
pub mod history;
|
||||
pub mod refs;
|
||||
|
||||
use banners::Banners;
|
||||
use files::RepoFilesView;
|
||||
use helpers::{
|
||||
RepoData, ShareTargets, bound_repo_label, clone_group, fork_row, load_repo_data,
|
||||
nostr_clone_url, repo_display_name,
|
||||
};
|
||||
use helpers::{RepoData, load_repo_data, repo_display_name};
|
||||
use history::RepoHistoryView;
|
||||
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;
|
||||
|
||||
@@ -65,16 +54,16 @@ pub enum RefKind {
|
||||
}
|
||||
|
||||
pub struct RepoDetailView {
|
||||
dock: WeakEntity<DockState>,
|
||||
store: Entity<RepoStore>,
|
||||
pub(super) dock: WeakEntity<DockState>,
|
||||
pub(super) store: Entity<RepoStore>,
|
||||
/// A repository opened by address alone starts without an announcement.
|
||||
repo_started: bool,
|
||||
/// The Files tab, which owns the explorer, previews and the per-file commit map.
|
||||
files: Entity<RepoFilesView>,
|
||||
/// The Commits tab, which walks HEAD and opens commit diffs.
|
||||
history: Entity<RepoHistoryView>,
|
||||
pub(super) history: Entity<RepoHistoryView>,
|
||||
/// The active tab: Files or Commits.
|
||||
active_tab: usize,
|
||||
pub(super) active_tab: usize,
|
||||
/// The checked-out worktree path.
|
||||
worktree: Option<PathBuf>,
|
||||
loading: bool,
|
||||
@@ -84,14 +73,14 @@ pub struct RepoDetailView {
|
||||
banners: Banners,
|
||||
tasks: Vec<Task<Result<(), Error>>>,
|
||||
/// Profiles rendered by the header and maintainers.
|
||||
directory: Entity<ProfileDirectory>,
|
||||
pub(super) directory: Entity<ProfileDirectory>,
|
||||
/// Sync status feeding the sync-to-Nostr banner.
|
||||
sync_directory: Entity<SyncStatusDirectory>,
|
||||
share_open: bool,
|
||||
repo_menu_open: bool,
|
||||
clone_open: bool,
|
||||
issues_menu_open: bool,
|
||||
pulls_menu_open: bool,
|
||||
pub(super) share_open: bool,
|
||||
pub(super) repo_menu_open: bool,
|
||||
pub(super) clone_open: bool,
|
||||
pub(super) issues_menu_open: bool,
|
||||
pub(super) pulls_menu_open: bool,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -151,10 +140,10 @@ impl RepoDetailView {
|
||||
|
||||
let store_for_attach = store.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
let _ = weak.update(cx, |this, cx| {
|
||||
crate::shared::log_dropped(weak.update(cx, |this, cx| {
|
||||
this.attach_store(&store_for_attach, window, cx);
|
||||
this.load_repo(window, cx);
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -406,8 +395,16 @@ impl RepoDetailView {
|
||||
return Ok::<_, Error>(None);
|
||||
};
|
||||
|
||||
repo.fetch().ok();
|
||||
let moved = repo.fast_forward_branches().unwrap_or(false);
|
||||
if let Err(error) = repo.fetch() {
|
||||
log::debug!("best-effort fetch failed: {error}");
|
||||
}
|
||||
let moved = match repo.fast_forward_branches() {
|
||||
Ok(moved) => moved,
|
||||
Err(error) => {
|
||||
log::warn!("failed to fast-forward branches: {error}");
|
||||
false
|
||||
}
|
||||
};
|
||||
Ok::<_, Error>(Some((moved, repo.snapshot()?)))
|
||||
})
|
||||
.await
|
||||
@@ -811,640 +808,83 @@ impl RepoDetailView {
|
||||
}
|
||||
|
||||
/// Open the upstream repository, the `u` tag of this fork's announcement.
|
||||
pub(super) fn open_upstream(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(addr) = self
|
||||
.announcement(cx)
|
||||
.and_then(|announcement| announcement.upstream.as_ref())
|
||||
.and_then(|upstream| upstream.addr.clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
fn ready_suggestion(&self, cx: &App) -> Option<CheckoutStatus> {
|
||||
let store = self.store.read(cx);
|
||||
let addr = store.addr()?;
|
||||
let user = Backend::global(cx).read(cx).current_user()?;
|
||||
|
||||
actions::open_repo_panel(&self.dock, &addr, None, window, cx);
|
||||
}
|
||||
|
||||
/// Open the dialog that publishes the local repository to NIP-34.
|
||||
fn open_init_dialog(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(local_path) = self.store.read(cx).path.clone() else {
|
||||
return;
|
||||
};
|
||||
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>) {
|
||||
let Some(addr) = self.store.read(cx).addr().cloned() else {
|
||||
return;
|
||||
};
|
||||
let id = SharedString::from(format!("issues:{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| IssuesView::new(dock_for_build.clone(), store.clone(), cx)),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn render_header(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||
if self.store.read(cx).addr().is_none() {
|
||||
return self.render_local_header(cx);
|
||||
if store.is_author(&user) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let store = self.store.read(cx);
|
||||
let pushing = store.pushing;
|
||||
let cloning = store.cloning;
|
||||
|
||||
let Some(announcement) = store.announcement.clone() else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
let nip05 = self
|
||||
.directory
|
||||
.read(cx)
|
||||
.profile(&announcement.owner, cx)
|
||||
.metadata()
|
||||
.nip05
|
||||
.clone()
|
||||
.filter(|nip05| !nip05.trim().is_empty());
|
||||
|
||||
let share = ShareTargets::from_announcement(&announcement);
|
||||
let nostr_url = nostr_clone_url(&announcement, nip05.as_deref());
|
||||
let ngit_command = SharedString::from(format!("git clone {nostr_url}"));
|
||||
let nak_command = SharedString::from(format!("nak git clone {nostr_url}"));
|
||||
let git_commands: Vec<String> = announcement.clone_urls();
|
||||
|
||||
let name = repo_display_name(self.store.read(cx));
|
||||
let description = announcement.description();
|
||||
let avatar = Avatar::new(format!("{}:{}", announcement.owner, announcement.id));
|
||||
|
||||
let share_naddr = share.naddr.clone();
|
||||
let share_for_menu = share.clone();
|
||||
let announcement_for_menu = announcement.clone();
|
||||
let weak_share = cx.entity().downgrade();
|
||||
let weak_menu_open = cx.entity().downgrade();
|
||||
let weak_menu_items = cx.entity().downgrade();
|
||||
let weak_clone = cx.entity().downgrade();
|
||||
let weak_download = cx.entity().downgrade();
|
||||
let ngit_for_popover = ngit_command.clone();
|
||||
let nak_for_popover = nak_command.clone();
|
||||
let git_for_popover = git_commands.clone();
|
||||
|
||||
v_flex()
|
||||
.flex_none()
|
||||
.p_4()
|
||||
.w_full()
|
||||
.gap_6()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors.border)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_4()
|
||||
.items_start()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||
.child(avatar)
|
||||
.child(name),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().colors.muted_foreground)
|
||||
.line_clamp(2)
|
||||
.child(description),
|
||||
)
|
||||
.when_some(fork_row(&announcement, cx), |this, row| this.child(row))
|
||||
.child(
|
||||
h_flex()
|
||||
.mt_2()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().colors.muted_foreground)
|
||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||
.child("Maintainers:"),
|
||||
)
|
||||
.child(self.render_maintainers(cx)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.gap_2()
|
||||
.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({
|
||||
let weak_issues = cx.entity().downgrade();
|
||||
let weak_issues_open = cx.entity().downgrade();
|
||||
let store_for_issues = self.store.clone();
|
||||
let issues_count = self.store.read(cx).issue_count();
|
||||
|
||||
DropdownMenu::new("issues")
|
||||
.label(format!("Issues {issues_count}"))
|
||||
.icon(Icon::named(CustomIconName::GitIssueDone))
|
||||
.variant(ButtonVariant::Secondary)
|
||||
.open(self.issues_menu_open)
|
||||
.on_click(move |_, window, cx| {
|
||||
if let Err(error) = weak_issues.update(cx, |this, cx| {
|
||||
this.open_issues(window, cx);
|
||||
}) {
|
||||
log::warn!("failed to open issues: {error}");
|
||||
}
|
||||
})
|
||||
.on_open_change(move |open, _, cx| {
|
||||
if let Err(error) =
|
||||
weak_issues_open.update(cx, |this, cx| {
|
||||
this.issues_menu_open = open;
|
||||
cx.notify();
|
||||
})
|
||||
{
|
||||
log::warn!("failed to update issues menu: {error}");
|
||||
}
|
||||
})
|
||||
.items(move |_, _| {
|
||||
vec![MenuItem::new("New issue").handler({
|
||||
let store = store_for_issues.clone();
|
||||
move |_, cx| {
|
||||
crate::issues::new_issue::open(store.clone(), cx);
|
||||
}
|
||||
})]
|
||||
})
|
||||
})
|
||||
.child(
|
||||
DropdownMenu::new("share")
|
||||
.icon(Icon::named(IconRole::Copy))
|
||||
.variant(ButtonVariant::Secondary)
|
||||
.open(self.share_open)
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
share_naddr.clone(),
|
||||
));
|
||||
})
|
||||
.on_open_change(move |open, _, cx| {
|
||||
if let Err(error) = weak_share.update(cx, |this, cx| {
|
||||
this.share_open = open;
|
||||
cx.notify();
|
||||
}) {
|
||||
log::warn!("failed to update share menu: {error}");
|
||||
}
|
||||
})
|
||||
.items(move |_, _| share_for_menu.menu()),
|
||||
)
|
||||
.child(
|
||||
DropdownMenu::new("repo-menu")
|
||||
.icon(Icon::named(CustomIconName::EllipsisVertical))
|
||||
.variant(ButtonVariant::Secondary)
|
||||
.loading(pushing)
|
||||
.disabled(pushing)
|
||||
.open(self.repo_menu_open)
|
||||
.on_open_change(move |open, _, cx| {
|
||||
if let Err(error) = weak_menu_open.update(cx, |this, cx| {
|
||||
this.repo_menu_open = open;
|
||||
cx.notify();
|
||||
}) {
|
||||
log::warn!("failed to update repo menu: {error}");
|
||||
}
|
||||
})
|
||||
.items(move |_, cx| {
|
||||
let owner = Backend::global(cx)
|
||||
.read(cx)
|
||||
.current_user()
|
||||
== Some(announcement_for_menu.owner);
|
||||
|
||||
let mut items = vec![MenuItem::new("About").handler({
|
||||
let weak = weak_menu_items.clone();
|
||||
let announcement = announcement_for_menu.clone();
|
||||
move |_, cx| {
|
||||
if let Err(error) = weak.update(cx, |this, cx| {
|
||||
dialogs::about::open(
|
||||
announcement.clone(),
|
||||
this.directory.clone(),
|
||||
cx,
|
||||
);
|
||||
}) {
|
||||
log::warn!("failed to open about: {error}");
|
||||
}
|
||||
}
|
||||
})];
|
||||
|
||||
if owner {
|
||||
items.push(MenuItem::new("Republish").handler({
|
||||
let weak = weak_menu_items.clone();
|
||||
move |window, cx| {
|
||||
if let Err(error) = weak.update(cx, |this, cx| {
|
||||
this.push_repository(window, cx);
|
||||
}) {
|
||||
log::warn!("failed to republish: {error}");
|
||||
}
|
||||
}
|
||||
}));
|
||||
items.push(MenuItem::new("Reset remote to local").handler({
|
||||
let weak = weak_menu_items.clone();
|
||||
move |window, cx| {
|
||||
if let Err(error) = weak.update(cx, |this, cx| {
|
||||
this.reset_remote(window, cx);
|
||||
}) {
|
||||
log::warn!("failed to reset remote: {error}");
|
||||
}
|
||||
}
|
||||
}));
|
||||
items.push(MenuItem::separator());
|
||||
items.push(
|
||||
MenuItem::new("Delete")
|
||||
.destructive(true)
|
||||
.handler({
|
||||
let weak = weak_menu_items.clone();
|
||||
move |window, cx| {
|
||||
if let Err(error) =
|
||||
weak.update(cx, |this, cx| {
|
||||
this.delete_repository(window, cx);
|
||||
})
|
||||
{
|
||||
log::warn!(
|
||||
"failed to delete repository: {error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
items
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Popover::new("clone")
|
||||
.open(self.clone_open)
|
||||
.on_change(move |open, _, cx| {
|
||||
if let Err(error) = weak_clone.update(cx, |this, cx| {
|
||||
this.clone_open = open;
|
||||
cx.notify();
|
||||
}) {
|
||||
log::warn!("failed to update clone popover: {error}");
|
||||
}
|
||||
})
|
||||
.trigger(move |_, _| {
|
||||
Button::new("clone")
|
||||
.icon(Icon::named(CustomIconName::GitClone))
|
||||
.tooltip(Tooltip::text("Clone"))
|
||||
.loading(cloning)
|
||||
.disabled(cloning)
|
||||
.into_any_element()
|
||||
})
|
||||
.content(move |_, cx| {
|
||||
v_flex()
|
||||
.w(px(440.))
|
||||
.p_3()
|
||||
.gap_4()
|
||||
.child(clone_group(
|
||||
"Clone with ngit",
|
||||
"copy-ngit",
|
||||
&ngit_for_popover,
|
||||
))
|
||||
.child(clone_group(
|
||||
"Clone with nak",
|
||||
"copy-nak",
|
||||
&nak_for_popover,
|
||||
))
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(
|
||||
cx.theme().colors.muted_foreground,
|
||||
)
|
||||
.child("Grasp Servers"),
|
||||
)
|
||||
.when(git_for_popover.is_empty(), |this| {
|
||||
this.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.child("No git clone urls."),
|
||||
)
|
||||
})
|
||||
.children(git_for_popover.iter().enumerate().map(
|
||||
|(ix, command)| {
|
||||
ClipboardButton::new(
|
||||
SharedString::from(format!(
|
||||
"copy-git-{ix}"
|
||||
)),
|
||||
command.clone(),
|
||||
)
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
h_flex().justify_end().child(
|
||||
Button::new("download")
|
||||
.icon(Icon::named(CustomIconName::GitClone))
|
||||
.label("Download")
|
||||
.on_click({
|
||||
let weak = weak_download.clone();
|
||||
move |_, window, cx| {
|
||||
if let Err(error) =
|
||||
weak.update(cx, |this, cx| {
|
||||
this.clone_to_folder(window, cx);
|
||||
})
|
||||
{
|
||||
log::warn!(
|
||||
"failed to clone: {error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(self.render_tabs(cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_local_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let name = repo_display_name(self.store.read(cx));
|
||||
let path = self
|
||||
.store
|
||||
.read(cx)
|
||||
.path
|
||||
.as_ref()
|
||||
.map(|path| path.display().to_string())
|
||||
.unwrap_or_default();
|
||||
let avatar = Avatar::new(path.clone());
|
||||
|
||||
let bound = self.store.read(cx).nip34.clone();
|
||||
let action = match bound
|
||||
.as_ref()
|
||||
.filter(|binding| binding.kind == Nip34Kind::Initialized)
|
||||
{
|
||||
Some(binding) => bound_repo_label(binding, &self.directory, cx),
|
||||
None => Button::new("init")
|
||||
.icon(Icon::named(CustomIconName::Init))
|
||||
.label("Initialize on Nostr")
|
||||
.tooltip(Tooltip::text("Publish this repository to Nostr"))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.open_init_dialog(window, cx);
|
||||
}))
|
||||
.into_any_element(),
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.flex_none()
|
||||
.px_4()
|
||||
.pb_4()
|
||||
.w_full()
|
||||
.gap_6()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors.border)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_4()
|
||||
.items_start()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||
.child(avatar)
|
||||
.child(name),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().colors.muted_foreground)
|
||||
.child(path),
|
||||
),
|
||||
)
|
||||
.child(action),
|
||||
)
|
||||
.child(self.render_tabs(cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_tabs(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let count = self.history.read(cx).commit_count();
|
||||
let muted = cx.theme().colors.muted_foreground;
|
||||
let weak = cx.entity().downgrade();
|
||||
let commits_label = match count {
|
||||
Some(count) => SharedString::from(format!("Commits ({count})")),
|
||||
None => SharedString::from("Commits"),
|
||||
};
|
||||
|
||||
Tabs::new("repo-tabs")
|
||||
.selected(self.active_tab)
|
||||
.tabs(move |_, _| {
|
||||
vec![
|
||||
Tab::new("files", "Files")
|
||||
.lead(Icon::named(CustomIconName::GitFile).color(muted)),
|
||||
Tab::new("commits", commits_label.clone())
|
||||
.lead(Icon::named(CustomIconName::GitCommit).color(muted)),
|
||||
]
|
||||
})
|
||||
.on_change(move |index, _, cx| {
|
||||
if let Err(error) = weak.update(cx, |this, cx| {
|
||||
this.active_tab = index;
|
||||
cx.notify();
|
||||
}) {
|
||||
log::warn!("failed to switch repo tab: {error}");
|
||||
let statuses = CheckoutsStore::global(cx).read(cx).ready_statuses_of(addr);
|
||||
'status: for status in statuses {
|
||||
if self.banners.dismissal(&status) {
|
||||
continue;
|
||||
}
|
||||
for pr in &store.pull_requests {
|
||||
if CheckoutsStore::pr_proposes_checkout(
|
||||
pr,
|
||||
store.status_of(pr) == signed_core::RepoStatus::Open,
|
||||
user,
|
||||
&status,
|
||||
) {
|
||||
continue 'status;
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
return Some(status);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn render_maintainers(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let Some(announcement) = self.announcement(cx) else {
|
||||
return div().into_any_element();
|
||||
fn render_ready_banner(&self, cx: &Context<Self>) -> Option<AnyElement> {
|
||||
let status = self.ready_suggestion(cx)?;
|
||||
let commits = if status.ahead == 1 {
|
||||
SharedString::from("1 commit")
|
||||
} else {
|
||||
SharedString::from(format!("{} commits", status.ahead))
|
||||
};
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
let rest: Vec<_> = announcement
|
||||
.maintainers
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|key| key != &announcement.owner && seen.insert(*key))
|
||||
.collect();
|
||||
|
||||
let owner = self.directory.read(cx).profile(&announcement.owner, cx);
|
||||
let owner_name = owner.name();
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_3()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Avatar::new(announcement.owner)
|
||||
.name(owner_name.clone())
|
||||
.picture(owner.picture())
|
||||
.small(),
|
||||
)
|
||||
.child(div().text_xs().whitespace_nowrap().child(owner_name)),
|
||||
)
|
||||
.when(!rest.is_empty(), |this| {
|
||||
this.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().colors.muted_foreground)
|
||||
.child(SharedString::from(format!("+{}", rest.len()))),
|
||||
Some(
|
||||
h_flex()
|
||||
.p_4()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.bg(cx.theme().colors.muted)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().colors.muted_foreground)
|
||||
.child(Badge::new("ready-branch", status.branch.clone()))
|
||||
.child(commits)
|
||||
.child("ahead of")
|
||||
.child(Badge::new("ready-base", status.base.clone())),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("create-pr-from-banner")
|
||||
.label("Create pull request")
|
||||
.small()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.open_new_pull_request(window, cx);
|
||||
})),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
.child(
|
||||
Button::new("dismiss-ready-banner")
|
||||
.icon(Icon::named(IconRole::Close))
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.banners.dismiss(&status);
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
fn push_suggestion(&self, cx: &App) -> Option<CheckoutStatus> {
|
||||
@@ -1758,7 +1198,8 @@ impl Panel for RepoDetailView {
|
||||
impl Render for RepoDetailView {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let banner = self
|
||||
.render_push_banner(cx)
|
||||
.render_ready_banner(cx)
|
||||
.or_else(|| self.render_push_banner(cx))
|
||||
.or_else(|| self.render_sync_banner(cx));
|
||||
|
||||
let error = self.error.clone().or_else(|| {
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::path::PathBuf;
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext as _, Entity, IntoElement, ParentElement as _, PathPromptOptions,
|
||||
SharedString, Styled as _, WeakEntity, Window, div, px,
|
||||
SharedString, Styled as _, Window, div, px,
|
||||
};
|
||||
use gpui_native_kit::button::Button;
|
||||
use gpui_native_kit::common::{h_flex, v_flex};
|
||||
@@ -13,7 +13,6 @@ use gpui_native_kit::theme::ActiveTheme as _;
|
||||
use gpui_native_kit::tooltip::Tooltip;
|
||||
use settings::{AppearanceMode, EventFetchingStrategy, SettingsStore};
|
||||
|
||||
use super::SettingsDialog;
|
||||
use crate::repo::dialogs::grasp_servers::{normalize_server, server_host};
|
||||
|
||||
/// A settings row with the label and description on the left and the control on the right.
|
||||
@@ -306,10 +305,7 @@ fn add_scan_path(cx: &mut App) {
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn default_folder_selector(
|
||||
default_folder: &Entity<InputState>,
|
||||
weak: WeakEntity<SettingsDialog>,
|
||||
) -> AnyElement {
|
||||
pub fn default_folder_selector(default_folder: &Entity<InputState>) -> AnyElement {
|
||||
let folder = default_folder.clone();
|
||||
h_flex()
|
||||
.gap_1()
|
||||
@@ -324,21 +320,15 @@ pub fn default_folder_selector(
|
||||
.variant(gpui_native_kit::button::ButtonVariant::Ghost)
|
||||
.tooltip(Tooltip::text("Choose folder"))
|
||||
.on_click(move |_, window, cx| {
|
||||
choose_default_folder(&folder, &weak, window, cx);
|
||||
choose_default_folder(&folder, window, cx);
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn choose_default_folder(
|
||||
default_folder: &Entity<InputState>,
|
||||
weak: &WeakEntity<SettingsDialog>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
fn choose_default_folder(default_folder: &Entity<InputState>, window: &mut Window, cx: &mut App) {
|
||||
let handle = window.window_handle();
|
||||
let default_folder = default_folder.clone();
|
||||
let weak = weak.clone();
|
||||
let prompt = cx.prompt_for_paths(PathPromptOptions {
|
||||
files: false,
|
||||
directories: true,
|
||||
@@ -359,7 +349,6 @@ fn choose_default_folder(
|
||||
);
|
||||
});
|
||||
default_folder.update(cx, |input, cx| input.set_value(text, window, cx));
|
||||
let _ = weak.update(cx, |_this, cx| cx.notify());
|
||||
}) {
|
||||
log::warn!("failed to apply default folder: {error}");
|
||||
}
|
||||
|
||||
@@ -46,10 +46,9 @@ impl SettingsDialog {
|
||||
}
|
||||
|
||||
impl Render for SettingsDialog {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let grasp_input = self.grasp_input.clone();
|
||||
let default_folder = self.default_folder.clone();
|
||||
let weak = cx.entity().downgrade();
|
||||
|
||||
Dialog::new("settings-dialog")
|
||||
.open(true)
|
||||
@@ -95,7 +94,7 @@ impl Render for SettingsDialog {
|
||||
cx,
|
||||
"Default Folder",
|
||||
"The folder for newly created repositories.",
|
||||
default_folder_selector(&default_folder, weak.clone()),
|
||||
default_folder_selector(&default_folder),
|
||||
))
|
||||
.into_any_element()
|
||||
})
|
||||
|
||||
@@ -37,6 +37,15 @@ impl DialogHost {
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DialogHost {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.when_some(self.open.clone(), |host, view| host.child(view))
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces the mounted modal with `view`.
|
||||
pub fn open_global(view: impl Into<AnyView>, cx: &mut App) {
|
||||
let host = DialogHost::global(cx);
|
||||
@@ -48,12 +57,3 @@ pub fn close_global(cx: &mut App) {
|
||||
let host = DialogHost::global(cx);
|
||||
host.update(cx, |host, cx| host.close(cx));
|
||||
}
|
||||
|
||||
impl Render for DialogHost {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.when_some(self.open.clone(), |host, view| host.child(view))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,3 +3,16 @@ pub mod panels;
|
||||
pub mod progress;
|
||||
pub mod tree;
|
||||
pub mod widgets;
|
||||
|
||||
/// Logs a discarded `Result` with its source location instead of dropping it silently.
|
||||
#[track_caller]
|
||||
pub fn log_dropped<T>(result: Result<T, anyhow::Error>) {
|
||||
if let Err(error) = result {
|
||||
let location = std::panic::Location::caller();
|
||||
log::warn!(
|
||||
"{}:{}: dropped result: {error}",
|
||||
location.file(),
|
||||
location.line()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,16 @@ use crate::settings;
|
||||
|
||||
mod helpers;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum SidebarSlot {
|
||||
Inbox,
|
||||
Browse,
|
||||
Search,
|
||||
Header,
|
||||
Repo(usize),
|
||||
Local(usize),
|
||||
}
|
||||
|
||||
pub struct SidebarView {
|
||||
weak: WeakEntity<Self>,
|
||||
dock: WeakEntity<DockState>,
|
||||
@@ -333,6 +343,10 @@ impl SidebarView {
|
||||
);
|
||||
}
|
||||
|
||||
debug_assert_eq!(
|
||||
entries.len(),
|
||||
slots(announcements.len(), local_repos.len()).len()
|
||||
);
|
||||
entries
|
||||
})
|
||||
.on_change(move |index, window, cx| {
|
||||
@@ -346,39 +360,34 @@ impl SidebarView {
|
||||
let Some(index) = index else {
|
||||
return;
|
||||
};
|
||||
|
||||
match index {
|
||||
0 => {
|
||||
self.open_inbox(window, cx);
|
||||
return;
|
||||
}
|
||||
1 | 2 => {
|
||||
self.open_explore(window, cx);
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let Some(row) = index.checked_sub(4) else {
|
||||
let Some(slot) = slots(self.announcements.len(), self.local_repos.len())
|
||||
.get(index)
|
||||
.copied()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if row < self.announcements.len() {
|
||||
let announcement = self.announcements[row].clone();
|
||||
let addr = announcement.addr();
|
||||
crate::repo::actions::open_repo_panel(
|
||||
&self.dock,
|
||||
&addr,
|
||||
Some(&announcement),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let local_ix = row - self.announcements.len();
|
||||
if let Some(entry) = self.local_repos.get(local_ix).cloned() {
|
||||
self.open_local_entry(entry, window, cx);
|
||||
match slot {
|
||||
SidebarSlot::Inbox => self.open_inbox(window, cx),
|
||||
SidebarSlot::Browse | SidebarSlot::Search => self.open_explore(window, cx),
|
||||
SidebarSlot::Header => {}
|
||||
SidebarSlot::Repo(ix) => {
|
||||
if let Some(announcement) = self.announcements.get(ix).cloned() {
|
||||
let addr = announcement.addr();
|
||||
crate::repo::actions::open_repo_panel(
|
||||
&self.dock,
|
||||
&addr,
|
||||
Some(&announcement),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
SidebarSlot::Local(ix) => {
|
||||
if let Some(entry) = self.local_repos.get(ix).cloned() {
|
||||
self.open_local_entry(entry, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,9 +465,9 @@ impl SidebarView {
|
||||
.label("Guide")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.on_click(move |_, window, cx| {
|
||||
let _ = guide_weak.update(cx, |this, cx| {
|
||||
crate::shared::log_dropped(guide_weak.update(cx, |this, cx| {
|
||||
this.open_explore(window, cx);
|
||||
});
|
||||
}));
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
@@ -660,3 +669,15 @@ impl SidebarView {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn slots(announcements: usize, local_repos: usize) -> Vec<SidebarSlot> {
|
||||
let mut slots = vec![
|
||||
SidebarSlot::Inbox,
|
||||
SidebarSlot::Browse,
|
||||
SidebarSlot::Search,
|
||||
SidebarSlot::Header,
|
||||
];
|
||||
slots.extend((0..announcements).map(SidebarSlot::Repo));
|
||||
slots.extend((0..local_repos).map(SidebarSlot::Local));
|
||||
slots
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user