clean up
Rust / build (macos-latest, stable) (push) Canceled after 0s
Rust / build (ubuntu-latest, stable) (push) Canceled after 0s
Rust / build (windows-latest, stable) (push) Canceled after 0s
Rust / build (macos-latest, stable) (pull_request) Canceled after 0s
Rust / build (ubuntu-latest, stable) (pull_request) Canceled after 0s
Rust / build (windows-latest, stable) (pull_request) Canceled after 0s
Rust / build (macos-latest, stable) (push) Canceled after 0s
Rust / build (ubuntu-latest, stable) (push) Canceled after 0s
Rust / build (windows-latest, stable) (push) Canceled after 0s
Rust / build (macos-latest, stable) (pull_request) Canceled after 0s
Rust / build (ubuntu-latest, stable) (pull_request) Canceled after 0s
Rust / build (windows-latest, stable) (pull_request) Canceled after 0s
This commit is contained in:
@@ -128,7 +128,9 @@ impl Render for OnboardingDialog {
|
|||||||
.loading(busy)
|
.loading(busy)
|
||||||
.disabled(busy)
|
.disabled(busy)
|
||||||
.on_click(move |_, _, cx| {
|
.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();
|
.into_any_element();
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ impl Render for PassphraseDialog {
|
|||||||
.loading(busy)
|
.loading(busy)
|
||||||
.disabled(busy)
|
.disabled(busy)
|
||||||
.on_click(move |_, _, cx| {
|
.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();
|
.into_any_element();
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use metadata::ProfileDirectory;
|
|||||||
use nostr::prelude::{Event, Kind, PublicKey};
|
use nostr::prelude::{Event, Kind, PublicKey};
|
||||||
use utils::relative_time;
|
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_flex()
|
||||||
.h_12()
|
.h_12()
|
||||||
.w_full()
|
.w_full()
|
||||||
@@ -27,7 +27,7 @@ pub fn repo_header(name: SharedString, unread: usize) -> AnyElement {
|
|||||||
.child(name),
|
.child(name),
|
||||||
)
|
)
|
||||||
.when(unread > 0, |this| {
|
.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()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,13 +27,6 @@ use crate::repo::actions::{RepoItem, open_repo_item};
|
|||||||
|
|
||||||
const MAX_SUB_ACTIVITIES: usize = 5;
|
const MAX_SUB_ACTIVITIES: usize = 5;
|
||||||
|
|
||||||
struct InboxSection {
|
|
||||||
address: Option<RepoAddr>,
|
|
||||||
unread: usize,
|
|
||||||
entries: Vec<usize>,
|
|
||||||
latest: Timestamp,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
enum InboxRow {
|
enum InboxRow {
|
||||||
Repo(usize),
|
Repo(usize),
|
||||||
@@ -41,12 +34,18 @@ enum InboxRow {
|
|||||||
Empty,
|
Empty,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct InboxSection {
|
||||||
|
address: Option<RepoAddr>,
|
||||||
|
unread: usize,
|
||||||
|
entries: Vec<usize>,
|
||||||
|
latest: Timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct InboxView {
|
pub struct InboxView {
|
||||||
dock: WeakEntity<DockState>,
|
dock: WeakEntity<DockState>,
|
||||||
threads: Arc<Vec<InboxItem>>,
|
threads: Arc<Vec<InboxItem>>,
|
||||||
sections: Arc<Vec<InboxSection>>,
|
sections: Arc<Vec<InboxSection>>,
|
||||||
rows: Arc<Vec<InboxRow>>,
|
rows: Arc<Vec<InboxRow>>,
|
||||||
unread_count: usize,
|
|
||||||
state: InboxReadState,
|
state: InboxReadState,
|
||||||
state_loaded: bool,
|
state_loaded: bool,
|
||||||
refresh: RefreshGate,
|
refresh: RefreshGate,
|
||||||
@@ -89,7 +88,6 @@ impl InboxView {
|
|||||||
threads: Arc::new(Vec::new()),
|
threads: Arc::new(Vec::new()),
|
||||||
sections: Arc::new(Vec::new()),
|
sections: Arc::new(Vec::new()),
|
||||||
rows: Arc::new(Vec::new()),
|
rows: Arc::new(Vec::new()),
|
||||||
unread_count: 0,
|
|
||||||
state: InboxReadState::default(),
|
state: InboxReadState::default(),
|
||||||
state_loaded: false,
|
state_loaded: false,
|
||||||
refresh: RefreshGate::default(),
|
refresh: RefreshGate::default(),
|
||||||
@@ -182,7 +180,7 @@ impl InboxView {
|
|||||||
let work = cx.background_spawn(async move { Inbox::query(&client, me, &state).await });
|
let work = cx.background_spawn(async move { Inbox::query(&client, me, &state).await });
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
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,
|
Ok(results) => results,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
log::warn!("inbox refresh failed: {error}");
|
log::warn!("inbox refresh failed: {error}");
|
||||||
@@ -196,7 +194,6 @@ impl InboxView {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.threads = Arc::new(threads);
|
this.threads = Arc::new(threads);
|
||||||
this.unread_count = unread_count;
|
|
||||||
this.rebuild(cx);
|
this.rebuild(cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
this.refresh.finish()
|
this.refresh.finish()
|
||||||
@@ -214,7 +211,6 @@ impl InboxView {
|
|||||||
for item in items.iter_mut() {
|
for item in items.iter_mut() {
|
||||||
item.apply_state(&self.state);
|
item.apply_state(&self.state);
|
||||||
}
|
}
|
||||||
self.unread_count = items.iter().filter(|item| item.is_unread()).count();
|
|
||||||
self.threads = Arc::new(items);
|
self.threads = Arc::new(items);
|
||||||
self.rebuild(cx);
|
self.rebuild(cx);
|
||||||
}
|
}
|
||||||
@@ -310,7 +306,6 @@ impl InboxView {
|
|||||||
self.threads = Arc::new(Vec::new());
|
self.threads = Arc::new(Vec::new());
|
||||||
self.sections = Arc::new(Vec::new());
|
self.sections = Arc::new(Vec::new());
|
||||||
self.rows = Arc::new(Vec::new());
|
self.rows = Arc::new(Vec::new());
|
||||||
self.unread_count = 0;
|
|
||||||
self.state = InboxReadState::default();
|
self.state = InboxReadState::default();
|
||||||
self.state_loaded = false;
|
self.state_loaded = false;
|
||||||
self.refresh = RefreshGate::default();
|
self.refresh = RefreshGate::default();
|
||||||
@@ -348,7 +343,15 @@ impl InboxView {
|
|||||||
return div().into_any_element();
|
return div().into_any_element();
|
||||||
};
|
};
|
||||||
let name = repo_name(section.address.as_ref(), cx);
|
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) => {
|
InboxRow::Entry(section_ix, entry_ix) => {
|
||||||
let Some(section) = self.sections.get(section_ix) else {
|
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 {
|
impl Panel for InboxView {
|
||||||
fn title(&self, _cx: &App) -> Option<SharedString> {
|
fn title(&self, _cx: &App) -> Option<SharedString> {
|
||||||
Some(SharedString::from("Inbox"))
|
Some(SharedString::from("Inbox"))
|
||||||
@@ -465,3 +455,22 @@ impl Render for InboxView {
|
|||||||
.into_any_element()
|
.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 directory = ProfileDirectory::new(cx);
|
||||||
let weak = cx.entity().downgrade();
|
let weak = cx.entity().downgrade();
|
||||||
cx.defer(move |cx| {
|
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 {
|
Self {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ impl IssuesView {
|
|||||||
});
|
});
|
||||||
let weak = cx.entity().downgrade();
|
let weak = cx.entity().downgrade();
|
||||||
cx.defer(move |cx| {
|
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 {
|
Self {
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ impl Render for NewIssueDialog {
|
|||||||
Button::new("create-issue")
|
Button::new("create-issue")
|
||||||
.label("Create issue")
|
.label("Create issue")
|
||||||
.on_click(move |_, _, cx| {
|
.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,
|
.into_any_element() as AnyElement,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ impl PullRequestDetailView {
|
|||||||
});
|
});
|
||||||
|
|
||||||
cx.defer(move |cx| {
|
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 {
|
Self {
|
||||||
@@ -291,7 +291,6 @@ impl PullRequestDetailView {
|
|||||||
None => SharedString::from(format!("commit:{}:{commit_id}", worktree.display())),
|
None => SharedString::from(format!("commit:{}:{commit_id}", worktree.display())),
|
||||||
};
|
};
|
||||||
let dock = self.dock.clone();
|
let dock = self.dock.clone();
|
||||||
let dock_for_build = dock.clone();
|
|
||||||
let store = self.store.clone();
|
let store = self.store.clone();
|
||||||
let repo_name = self.repo_name.clone();
|
let repo_name = self.repo_name.clone();
|
||||||
let commit_id = commit_id.to_owned();
|
let commit_id = commit_id.to_owned();
|
||||||
@@ -313,7 +312,6 @@ impl PullRequestDetailView {
|
|||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
let _ = dock_for_build;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_tabs(&self, cx: &mut Context<Self>) -> AnyElement {
|
fn render_tabs(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
@@ -339,10 +337,10 @@ impl PullRequestDetailView {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
.on_change(move |index, _, cx| {
|
.on_change(move |index, _, cx| {
|
||||||
let _ = weak.update(cx, |this, cx| {
|
crate::shared::log_dropped(weak.update(cx, |this, cx| {
|
||||||
this.active_tab = index;
|
this.active_tab = index;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
}));
|
||||||
})
|
})
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
@@ -476,9 +474,9 @@ impl PullRequestDetailView {
|
|||||||
&commit,
|
&commit,
|
||||||
move |window, cx| {
|
move |window, cx| {
|
||||||
if let Some(worktree) = worktree.clone() {
|
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);
|
this.open_commit_diff(worktree, &id, window, cx);
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
@@ -555,7 +553,7 @@ impl PullRequestDetailView {
|
|||||||
.when(can_update, |this| {
|
.when(can_update, |this| {
|
||||||
this.child(Button::new("update-pr").label("Update").small().on_click(
|
this.child(Button::new("update-pr").label("Update").small().on_click(
|
||||||
move |_, _, cx| {
|
move |_, _, cx| {
|
||||||
let _ = weak.update(cx, |this, cx| {
|
crate::shared::log_dropped(weak.update(cx, |this, cx| {
|
||||||
let root = this
|
let root = this
|
||||||
.store
|
.store
|
||||||
.read(cx)
|
.read(cx)
|
||||||
@@ -568,7 +566,7 @@ impl PullRequestDetailView {
|
|||||||
if let Some(root) = root {
|
if let Some(root) = root {
|
||||||
dialogs::update::open(this.store.clone(), root, cx);
|
dialogs::update::open(this.store.clone(), root, cx);
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,10 +13,6 @@ use signed_state::RepoStore;
|
|||||||
|
|
||||||
use crate::shared::dialogs::{close_global, open_global};
|
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 {
|
pub struct UpdatePrDialog {
|
||||||
store: Entity<RepoStore>,
|
store: Entity<RepoStore>,
|
||||||
root: Event,
|
root: Event,
|
||||||
@@ -76,10 +72,14 @@ impl Render for UpdatePrDialog {
|
|||||||
Button::new("submit-update")
|
Button::new("submit-update")
|
||||||
.label("Update pull request")
|
.label("Update pull request")
|
||||||
.on_click(move |_, _, cx| {
|
.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,
|
.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 std::path::PathBuf;
|
||||||
use signed_core::RepoStatus;
|
|
||||||
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum PullRequestFilter {
|
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 struct ForkCompare {
|
||||||
pub announcement: Announcement,
|
pub announcement: Announcement,
|
||||||
pub namespace: String,
|
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 {
|
pub fn fork_display_name(announcement: &Announcement) -> SharedString {
|
||||||
announcement
|
announcement
|
||||||
.name
|
.name
|
||||||
@@ -77,3 +78,74 @@ pub fn shorten_owner(owner: &PublicKey) -> String {
|
|||||||
pub fn truncate_label(label: &str) -> SharedString {
|
pub fn truncate_label(label: &str) -> SharedString {
|
||||||
SharedString::from(middle_truncate(label, 18, 0))
|
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| {
|
cx.defer(move |cx| {
|
||||||
let _ = weak.update(cx, |this, cx| {
|
crate::shared::log_dropped(weak.update(cx, |this, cx| {
|
||||||
this.rebuild(cx);
|
this.rebuild(cx);
|
||||||
});
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -255,18 +255,18 @@ impl PullRequestsView {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on_open_change(move |open, _, cx| {
|
.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;
|
this.new_pr_menu_open = open;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
}));
|
||||||
})
|
})
|
||||||
.items(move |_, _| {
|
.items(move |_, _| {
|
||||||
vec![MenuItem::new("Send Patch").handler({
|
vec![MenuItem::new("Send Patch").handler({
|
||||||
let weak = patch_weak.clone();
|
let weak = patch_weak.clone();
|
||||||
move |window, cx| {
|
move |window, cx| {
|
||||||
let _ = weak.update(cx, |this, cx| {
|
crate::shared::log_dropped(weak.update(cx, |this, cx| {
|
||||||
this.open_send_patch(window, cx);
|
this.open_send_patch(window, cx);
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
})]
|
})]
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use assets::CustomIconName;
|
use assets::CustomIconName;
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement,
|
AnyElement, App, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement,
|
||||||
ParentElement as _, PathPromptOptions, Render, SharedString, Styled as _, Subscription,
|
ParentElement as _, Render, SharedString, Styled as _, Subscription, UniformListScrollHandle,
|
||||||
UniformListScrollHandle, WeakEntity, Window, div, px, uniform_list,
|
WeakEntity, Window, div, px, uniform_list,
|
||||||
};
|
};
|
||||||
use gpui_native_kit::button::Button;
|
use gpui_native_kit::button::Button;
|
||||||
use gpui_native_kit::combobox::{Combobox, ComboboxState};
|
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::icons::{Icon, IconRole};
|
||||||
use gpui_native_kit::input::{Input, InputState};
|
use gpui_native_kit::input::{Input, InputState};
|
||||||
use gpui_native_kit::layout::dock::{DockState, Panel};
|
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::scrollbar::Scrollbar;
|
||||||
use gpui_native_kit::spinner::Spinner;
|
use gpui_native_kit::spinner::Spinner;
|
||||||
use gpui_native_kit::theme::ActiveTheme as _;
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
use metadata::ProfileDirectory;
|
use metadata::ProfileDirectory;
|
||||||
use nostr::prelude::Url;
|
use signed_state::{CheckoutsStore, RepoStore};
|
||||||
use signed_core::Announcement;
|
|
||||||
use signed_git::{GitCache, Repo};
|
|
||||||
use signed_state::{Backend, CheckoutsStore, Mirrors, RepoListStore, RepoStore};
|
|
||||||
|
|
||||||
use super::helpers::{ForkCompare, fork_display_name, shorten_owner, truncate_label};
|
use super::helpers::{ForkCompare, fork_display_name, truncate_label};
|
||||||
use crate::repo::commit_diff::{CommitDiffView, DiffPane};
|
use crate::repo::commit_diff::{CommitDiffView, DiffPane};
|
||||||
use crate::repo::helpers::commit_row;
|
use crate::repo::helpers::commit_row;
|
||||||
use crate::shared::widgets::empty_state;
|
use crate::shared::widgets::empty_state;
|
||||||
|
|
||||||
|
mod source;
|
||||||
|
|
||||||
pub struct NewPullRequestView {
|
pub struct NewPullRequestView {
|
||||||
dock: WeakEntity<DockState>,
|
dock: WeakEntity<DockState>,
|
||||||
panel_id: SharedString,
|
panel_id: SharedString,
|
||||||
store: Entity<RepoStore>,
|
pub(super) store: Entity<RepoStore>,
|
||||||
repo_name: SharedString,
|
repo_name: SharedString,
|
||||||
repo_path: Option<PathBuf>,
|
pub(super) repo_path: Option<PathBuf>,
|
||||||
branches: Vec<SharedString>,
|
pub(super) branches: Vec<SharedString>,
|
||||||
fork: Option<ForkCompare>,
|
pub(super) fork: Option<ForkCompare>,
|
||||||
base: SharedString,
|
pub(super) base: SharedString,
|
||||||
compare: SharedString,
|
pub(super) compare: SharedString,
|
||||||
base_select: Entity<ComboboxState<SharedString>>,
|
pub(super) base_select: Entity<ComboboxState<SharedString>>,
|
||||||
compare_select: Entity<ComboboxState<SharedString>>,
|
pub(super) compare_select: Entity<ComboboxState<SharedString>>,
|
||||||
subject: Entity<InputState>,
|
subject: Entity<InputState>,
|
||||||
description: Entity<InputState>,
|
description: Entity<InputState>,
|
||||||
merge_base: Option<String>,
|
pub(super) merge_base: Option<String>,
|
||||||
commits: Option<Vec<signed_git::FileCommit>>,
|
pub(super) commits: Option<Vec<signed_git::FileCommit>>,
|
||||||
loading: bool,
|
pub(super) loading: bool,
|
||||||
error: Option<SharedString>,
|
pub(super) error: Option<SharedString>,
|
||||||
submitting: bool,
|
submitting: bool,
|
||||||
compare_generation: u64,
|
pub(super) compare_generation: u64,
|
||||||
|
pub(super) fork_generation: u64,
|
||||||
active_tab: usize,
|
active_tab: usize,
|
||||||
pane: Entity<DiffPane>,
|
pub(super) pane: Entity<DiffPane>,
|
||||||
scroll_handle: UniformListScrollHandle,
|
scroll_handle: UniformListScrollHandle,
|
||||||
tasks: Vec<gpui::Task<Result<(), anyhow::Error>>>,
|
pub(super) tasks: Vec<gpui::Task<Result<(), anyhow::Error>>>,
|
||||||
directory: Entity<ProfileDirectory>,
|
directory: Entity<ProfileDirectory>,
|
||||||
source_open: bool,
|
source_open: bool,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
@@ -80,10 +80,13 @@ impl NewPullRequestView {
|
|||||||
let weak = weak.clone();
|
let weak = weak.clone();
|
||||||
move |value, _window, cx| {
|
move |value, _window, cx| {
|
||||||
if let Some(base) = value {
|
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.base = base;
|
||||||
this.reload_compare(cx);
|
this.reload_compare(cx);
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -98,10 +101,13 @@ impl NewPullRequestView {
|
|||||||
let weak = weak.clone();
|
let weak = weak.clone();
|
||||||
move |value, _window, cx| {
|
move |value, _window, cx| {
|
||||||
if let Some(compare) = value {
|
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.compare = compare;
|
||||||
this.reload_compare(cx);
|
this.reload_compare(cx);
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -115,7 +121,7 @@ impl NewPullRequestView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.defer(cx, move |window, cx| {
|
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 {
|
let Some(addr) = this.store.read(cx).addr().cloned() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -130,7 +136,7 @@ impl NewPullRequestView {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.apply_folder_path(path, window, cx);
|
this.apply_folder_path(path, window, cx);
|
||||||
});
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -153,6 +159,7 @@ impl NewPullRequestView {
|
|||||||
error: None,
|
error: None,
|
||||||
submitting: false,
|
submitting: false,
|
||||||
compare_generation: 0,
|
compare_generation: 0,
|
||||||
|
fork_generation: 0,
|
||||||
active_tab: 0,
|
active_tab: 0,
|
||||||
pane,
|
pane,
|
||||||
scroll_handle: UniformListScrollHandle::new(),
|
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>) {
|
fn submit(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if self.submitting || self.loading {
|
if self.submitting || self.loading {
|
||||||
return;
|
return;
|
||||||
@@ -566,7 +216,7 @@ impl NewPullRequestView {
|
|||||||
this.update(cx, |this, _cx| {
|
this.update(cx, |this, _cx| {
|
||||||
this.submitting = false;
|
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(())
|
Ok(())
|
||||||
});
|
});
|
||||||
self.tasks.push(task);
|
self.tasks.push(task);
|
||||||
@@ -619,7 +269,19 @@ impl NewPullRequestView {
|
|||||||
fn render_compare_bar(&self, cx: &mut Context<Self>) -> AnyElement {
|
fn render_compare_bar(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let weak = cx.entity().downgrade();
|
let weak = cx.entity().downgrade();
|
||||||
let weak_menu = 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 can_submit = self.submit_enabled(cx);
|
||||||
let source_label = match &self.fork {
|
let source_label = match &self.fork {
|
||||||
Some(fork) => truncate_label(&fork_display_name(&fork.announcement)),
|
Some(fork) => truncate_label(&fork_display_name(&fork.announcement)),
|
||||||
@@ -653,10 +315,10 @@ impl NewPullRequestView {
|
|||||||
Menu::new("pr-source")
|
Menu::new("pr-source")
|
||||||
.open(self.source_open)
|
.open(self.source_open)
|
||||||
.on_change(move |open, _, cx| {
|
.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;
|
this.source_open = open;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
}));
|
||||||
})
|
})
|
||||||
.trigger(move |_, _| {
|
.trigger(move |_, _| {
|
||||||
Button::new("compare-source")
|
Button::new("compare-source")
|
||||||
@@ -664,7 +326,16 @@ impl NewPullRequestView {
|
|||||||
.variant(gpui_native_kit::button::ButtonVariant::Secondary)
|
.variant(gpui_native_kit::button::ButtonVariant::Secondary)
|
||||||
.into_any_element()
|
.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| {
|
.when_some(refresh_fork, |this, fork| {
|
||||||
@@ -684,76 +355,14 @@ impl NewPullRequestView {
|
|||||||
.loading(self.submitting)
|
.loading(self.submitting)
|
||||||
.disabled(!can_submit)
|
.disabled(!can_submit)
|
||||||
.on_click(move |_, window, cx| {
|
.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()
|
.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 {
|
fn render_tabs(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let files = self.pane.read(cx).diff().map_or(0, |diff| diff.files.len());
|
let files = self.pane.read(cx).diff().map_or(0, |diff| diff.files.len());
|
||||||
let commits = self.commits.as_ref().map_or(0, |commits| commits.len());
|
let commits = self.commits.as_ref().map_or(0, |commits| commits.len());
|
||||||
@@ -770,10 +379,10 @@ impl NewPullRequestView {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
.on_change(move |index, _, cx| {
|
.on_change(move |index, _, cx| {
|
||||||
let _ = weak.update(cx, |this, cx| {
|
crate::shared::log_dropped(weak.update(cx, |this, cx| {
|
||||||
this.active_tab = index;
|
this.active_tab = index;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
}));
|
||||||
})
|
})
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
@@ -824,9 +433,9 @@ impl NewPullRequestView {
|
|||||||
ix,
|
ix,
|
||||||
&commit,
|
&commit,
|
||||||
move |window, cx| {
|
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);
|
this.open_commit_diff(&id, window, cx);
|
||||||
});
|
}));
|
||||||
},
|
},
|
||||||
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 panel_id = self.panel_id.clone();
|
||||||
let _ = self
|
crate::shared::log_dropped(
|
||||||
.dock
|
self.dock
|
||||||
.update(cx, |dock, cx| dock.close_panel(&panel_id, cx));
|
.update(cx, |dock, cx| dock.close_panel(&panel_id, cx)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_footer(&self, cx: &Context<Self>) -> AnyElement {
|
fn render_footer(&self, cx: &Context<Self>) -> AnyElement {
|
||||||
@@ -127,7 +128,7 @@ impl SendPatchView {
|
|||||||
.loading(self.submitting)
|
.loading(self.submitting)
|
||||||
.disabled(!can_submit)
|
.disabled(!can_submit)
|
||||||
.on_click(move |_, _, cx| {
|
.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()
|
.into_any_element()
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ use signed_state::RepoStore;
|
|||||||
use crate::repo::RepoDetailView;
|
use crate::repo::RepoDetailView;
|
||||||
use crate::shared::panels;
|
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.
|
/// Opens a repository as a dock panel, keyed by its address.
|
||||||
pub fn open_repo_panel(
|
pub fn open_repo_panel(
|
||||||
dock: &WeakEntity<DockState>,
|
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.
|
/// Opens an issue or pull request using a store built here.
|
||||||
pub fn open_repo_item(
|
pub fn open_repo_item(
|
||||||
dock: &WeakEntity<DockState>,
|
dock: &WeakEntity<DockState>,
|
||||||
|
|||||||
@@ -222,7 +222,9 @@ impl Render for CreateRepoDialog {
|
|||||||
.loading(busy)
|
.loading(busy)
|
||||||
.disabled(busy)
|
.disabled(busy)
|
||||||
.on_click(move |_, window, cx| {
|
.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();
|
.into_any_element();
|
||||||
|
|
||||||
|
|||||||
@@ -243,7 +243,9 @@ impl Render for InitRepoDialog {
|
|||||||
.loading(busy)
|
.loading(busy)
|
||||||
.disabled(busy)
|
.disabled(busy)
|
||||||
.on_click(move |_, window, cx| {
|
.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();
|
.into_any_element();
|
||||||
|
|
||||||
|
|||||||
@@ -69,8 +69,7 @@ pub struct RepoFilesView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RepoFilesView {
|
impl RepoFilesView {
|
||||||
pub fn new(cx: &mut Context<Self>) -> Self {
|
pub fn new(_cx: &mut Context<Self>) -> Self {
|
||||||
let _ = cx;
|
|
||||||
Self {
|
Self {
|
||||||
seeds: Vec::new(),
|
seeds: Vec::new(),
|
||||||
expanded: HashSet::new(),
|
expanded: HashSet::new(),
|
||||||
@@ -448,13 +447,7 @@ impl RepoFilesView {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_code(
|
fn set_code(&mut self, path: SharedString, text: &str, cx: &mut Context<Self>) {
|
||||||
&mut self,
|
|
||||||
path: SharedString,
|
|
||||||
text: &str,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let hash = source_hash(text);
|
let hash = source_hash(text);
|
||||||
let language = code_language(path.as_ref());
|
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 state = cx.new(|cx| CodeViewState::new("repo-code", text, language, cx));
|
||||||
let _ = window;
|
|
||||||
self.code = Some(CodeView {
|
self.code = Some(CodeView {
|
||||||
path,
|
path,
|
||||||
state,
|
state,
|
||||||
@@ -514,7 +506,7 @@ impl RepoFilesView {
|
|||||||
self.set_markdown(Some(path.into()), &text, cx);
|
self.set_markdown(Some(path.into()), &text, cx);
|
||||||
}
|
}
|
||||||
} else if self.code.as_ref().map(|code| code.path.as_str()) != Some(path) {
|
} 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();
|
cx.notify();
|
||||||
@@ -574,7 +566,7 @@ impl RepoFilesView {
|
|||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, _window, cx| {
|
||||||
this.loading_files.remove(&path);
|
this.loading_files.remove(&path);
|
||||||
|
|
||||||
match content {
|
match content {
|
||||||
@@ -590,7 +582,7 @@ impl RepoFilesView {
|
|||||||
let same = this.code.as_ref().map(|code| code.path.as_str())
|
let same = this.code.as_ref().map(|code| code.path.as_str())
|
||||||
== Some(path.as_str());
|
== Some(path.as_str());
|
||||||
if !same {
|
if !same {
|
||||||
this.set_code(path.clone().into(), text, window, cx);
|
this.set_code(path.clone().into(), text, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.preview_bytes += text.len();
|
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 super::RepoDetailView;
|
||||||
use crate::shared::tree::{TreeItemSeed, build_tree_items};
|
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) struct RepoData {
|
||||||
pub(super) tree: Vec<TreeItemSeed>,
|
pub(super) tree: Vec<TreeItemSeed>,
|
||||||
pub(super) entries: Vec<PathBuf>,
|
pub(super) entries: Vec<PathBuf>,
|
||||||
@@ -38,6 +56,67 @@ pub(super) struct RepoData {
|
|||||||
pub(super) head_commit: Option<FileCommit>,
|
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 {
|
pub(super) fn clone_group(label: &str, id: &'static str, command: &SharedString) -> AnyElement {
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_1()
|
.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 {
|
pub(super) fn truncate_naddr_link(url: &str, tail: usize) -> String {
|
||||||
let Some(end) = url.find("naddr1").map(|i| i + "naddr1".len()) else {
|
let Some(end) = url.find("naddr1").map(|i| i + "naddr1".len()) else {
|
||||||
return url.to_string();
|
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> {
|
pub(super) fn diff_rows(file: &FileDiff) -> Vec<DiffRow> {
|
||||||
let mut rows = Vec::new();
|
let mut rows = Vec::new();
|
||||||
for (hunk_ix, hunk) in file.hunks.iter().enumerate() {
|
for (hunk_ix, hunk) in file.hunks.iter().enumerate() {
|
||||||
|
|||||||
@@ -5,32 +5,25 @@ use anyhow::Error;
|
|||||||
use assets::CustomIconName;
|
use assets::CustomIconName;
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AppContext as _, ClipboardItem, Context, Entity, InteractiveElement as _,
|
AnyElement, App, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement,
|
||||||
IntoElement, ParentElement as _, PathPromptOptions, Render, SharedString, Styled as _,
|
ParentElement as _, PathPromptOptions, Render, SharedString, Styled as _, Subscription, Task,
|
||||||
Subscription, Task, WeakEntity, Window, div, px,
|
WeakEntity, Window, div, px,
|
||||||
};
|
};
|
||||||
use gpui_native_kit::alert::{Alert, AlertSeverity};
|
use gpui_native_kit::alert::{Alert, AlertSeverity};
|
||||||
use gpui_native_kit::avatar::Avatar;
|
|
||||||
use gpui_native_kit::badge::Badge;
|
use gpui_native_kit::badge::Badge;
|
||||||
use gpui_native_kit::button::{Button, ButtonVariant};
|
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::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::icons::{Icon, IconRole};
|
||||||
use gpui_native_kit::layout::dock::{DockState, Panel};
|
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::spinner::Spinner;
|
||||||
use gpui_native_kit::theme::ActiveTheme as _;
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
use gpui_native_kit::tooltip::Tooltip;
|
|
||||||
use metadata::ProfileDirectory;
|
use metadata::ProfileDirectory;
|
||||||
use nostr::prelude::{PublicKey, Url};
|
use nostr::prelude::{PublicKey, Url};
|
||||||
use signed_core::{Announcement, RepoAddr};
|
use signed_core::{Announcement, RepoAddr};
|
||||||
use signed_git::{FileCommit, GitCache, Repo};
|
use signed_git::{FileCommit, GitCache, Repo};
|
||||||
use signed_state::{
|
use signed_state::{
|
||||||
Backend, CheckoutStatus, CheckoutSyncStatus, CheckoutsStore, LocalReposStore, Mirrors,
|
Backend, CheckoutStatus, CheckoutSyncStatus, CheckoutsStore, LocalReposStore, Mirrors,
|
||||||
Nip34Binding, Nip34Kind, PushStepStatus, RepoStore, SyncStatusDirectory,
|
Nip34Binding, PushStepStatus, RepoStore, SyncStatusDirectory,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod actions;
|
pub mod actions;
|
||||||
@@ -38,21 +31,17 @@ pub mod banners;
|
|||||||
pub mod commit_diff;
|
pub mod commit_diff;
|
||||||
pub mod dialogs;
|
pub mod dialogs;
|
||||||
pub mod files;
|
pub mod files;
|
||||||
|
mod header;
|
||||||
pub mod helpers;
|
pub mod helpers;
|
||||||
pub mod history;
|
pub mod history;
|
||||||
pub mod refs;
|
pub mod refs;
|
||||||
|
|
||||||
use banners::Banners;
|
use banners::Banners;
|
||||||
use files::RepoFilesView;
|
use files::RepoFilesView;
|
||||||
use helpers::{
|
use helpers::{RepoData, load_repo_data, repo_display_name};
|
||||||
RepoData, ShareTargets, bound_repo_label, clone_group, fork_row, load_repo_data,
|
|
||||||
nostr_clone_url, repo_display_name,
|
|
||||||
};
|
|
||||||
use history::RepoHistoryView;
|
use history::RepoHistoryView;
|
||||||
use refs::RefSwitcher;
|
use refs::RefSwitcher;
|
||||||
|
|
||||||
use crate::issues::IssuesView;
|
|
||||||
use crate::pull_requests::{NewPullRequestView, PullRequestsView, SendPatchView};
|
|
||||||
use crate::shared::tree::{build_tree_items, sorted_worktree_paths};
|
use crate::shared::tree::{build_tree_items, sorted_worktree_paths};
|
||||||
use crate::shared::widgets::repo_tab_avatar;
|
use crate::shared::widgets::repo_tab_avatar;
|
||||||
|
|
||||||
@@ -65,16 +54,16 @@ pub enum RefKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct RepoDetailView {
|
pub struct RepoDetailView {
|
||||||
dock: WeakEntity<DockState>,
|
pub(super) dock: WeakEntity<DockState>,
|
||||||
store: Entity<RepoStore>,
|
pub(super) store: Entity<RepoStore>,
|
||||||
/// A repository opened by address alone starts without an announcement.
|
/// A repository opened by address alone starts without an announcement.
|
||||||
repo_started: bool,
|
repo_started: bool,
|
||||||
/// The Files tab, which owns the explorer, previews and the per-file commit map.
|
/// The Files tab, which owns the explorer, previews and the per-file commit map.
|
||||||
files: Entity<RepoFilesView>,
|
files: Entity<RepoFilesView>,
|
||||||
/// The Commits tab, which walks HEAD and opens commit diffs.
|
/// The Commits tab, which walks HEAD and opens commit diffs.
|
||||||
history: Entity<RepoHistoryView>,
|
pub(super) history: Entity<RepoHistoryView>,
|
||||||
/// The active tab: Files or Commits.
|
/// The active tab: Files or Commits.
|
||||||
active_tab: usize,
|
pub(super) active_tab: usize,
|
||||||
/// The checked-out worktree path.
|
/// The checked-out worktree path.
|
||||||
worktree: Option<PathBuf>,
|
worktree: Option<PathBuf>,
|
||||||
loading: bool,
|
loading: bool,
|
||||||
@@ -84,14 +73,14 @@ pub struct RepoDetailView {
|
|||||||
banners: Banners,
|
banners: Banners,
|
||||||
tasks: Vec<Task<Result<(), Error>>>,
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
/// Profiles rendered by the header and maintainers.
|
/// Profiles rendered by the header and maintainers.
|
||||||
directory: Entity<ProfileDirectory>,
|
pub(super) directory: Entity<ProfileDirectory>,
|
||||||
/// Sync status feeding the sync-to-Nostr banner.
|
/// Sync status feeding the sync-to-Nostr banner.
|
||||||
sync_directory: Entity<SyncStatusDirectory>,
|
sync_directory: Entity<SyncStatusDirectory>,
|
||||||
share_open: bool,
|
pub(super) share_open: bool,
|
||||||
repo_menu_open: bool,
|
pub(super) repo_menu_open: bool,
|
||||||
clone_open: bool,
|
pub(super) clone_open: bool,
|
||||||
issues_menu_open: bool,
|
pub(super) issues_menu_open: bool,
|
||||||
pulls_menu_open: bool,
|
pub(super) pulls_menu_open: bool,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,10 +140,10 @@ impl RepoDetailView {
|
|||||||
|
|
||||||
let store_for_attach = store.clone();
|
let store_for_attach = store.clone();
|
||||||
window.defer(cx, move |window, cx| {
|
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.attach_store(&store_for_attach, window, cx);
|
||||||
this.load_repo(window, cx);
|
this.load_repo(window, cx);
|
||||||
});
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -406,8 +395,16 @@ impl RepoDetailView {
|
|||||||
return Ok::<_, Error>(None);
|
return Ok::<_, Error>(None);
|
||||||
};
|
};
|
||||||
|
|
||||||
repo.fetch().ok();
|
if let Err(error) = repo.fetch() {
|
||||||
let moved = repo.fast_forward_branches().unwrap_or(false);
|
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()?)))
|
Ok::<_, Error>(Some((moved, repo.snapshot()?)))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -811,640 +808,83 @@ impl RepoDetailView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Open the upstream repository, the `u` tag of this fork's announcement.
|
/// 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>) {
|
fn ready_suggestion(&self, cx: &App) -> Option<CheckoutStatus> {
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
let store = self.store.read(cx);
|
let store = self.store.read(cx);
|
||||||
let pushing = store.pushing;
|
let addr = store.addr()?;
|
||||||
let cloning = store.cloning;
|
let user = Backend::global(cx).read(cx).current_user()?;
|
||||||
|
|
||||||
let Some(announcement) = store.announcement.clone() else {
|
if store.is_author(&user) {
|
||||||
return div().into_any_element();
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Some(status);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
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 nip05 = self
|
Some(
|
||||||
.directory
|
h_flex()
|
||||||
.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()
|
.p_4()
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_6()
|
|
||||||
.border_b_1()
|
|
||||||
.border_color(cx.theme().colors.border)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.w_full()
|
|
||||||
.gap_4()
|
|
||||||
.items_start()
|
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(
|
.bg(cx.theme().colors.muted)
|
||||||
v_flex()
|
|
||||||
.flex_1()
|
|
||||||
.min_w_0()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
|
||||||
.child(avatar)
|
|
||||||
.child(name),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.min_w_0()
|
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.text_color(cx.theme().colors.muted_foreground)
|
.text_color(cx.theme().colors.muted_foreground)
|
||||||
.line_clamp(2)
|
.child(Badge::new("ready-branch", status.branch.clone()))
|
||||||
.child(description),
|
.child(commits)
|
||||||
)
|
.child("ahead of")
|
||||||
.when_some(fork_row(&announcement, cx), |this, row| this.child(row))
|
.child(Badge::new("ready-base", status.base.clone())),
|
||||||
.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(
|
.child(
|
||||||
h_flex()
|
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()
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
div()
|
Button::new("create-pr-from-banner")
|
||||||
.text_xs()
|
.label("Create pull request")
|
||||||
.text_color(
|
.small()
|
||||||
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| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
this.open_init_dialog(window, cx);
|
this.open_new_pull_request(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(
|
.child(
|
||||||
div()
|
Button::new("dismiss-ready-banner")
|
||||||
.min_w_0()
|
.icon(Icon::named(IconRole::Close))
|
||||||
.text_sm()
|
.variant(ButtonVariant::Ghost)
|
||||||
.text_color(cx.theme().colors.muted_foreground)
|
.small()
|
||||||
.child(path),
|
.on_click(cx.listener(move |this, _, _, cx| {
|
||||||
|
this.banners.dismiss(&status);
|
||||||
|
cx.notify();
|
||||||
|
})),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(action),
|
.into_any_element(),
|
||||||
)
|
)
|
||||||
.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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_suggestion(&self, cx: &App) -> Option<CheckoutStatus> {
|
fn push_suggestion(&self, cx: &App) -> Option<CheckoutStatus> {
|
||||||
@@ -1758,7 +1198,8 @@ impl Panel for RepoDetailView {
|
|||||||
impl Render for RepoDetailView {
|
impl Render for RepoDetailView {
|
||||||
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 banner = self
|
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));
|
.or_else(|| self.render_sync_banner(cx));
|
||||||
|
|
||||||
let error = self.error.clone().or_else(|| {
|
let error = self.error.clone().or_else(|| {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AppContext as _, Entity, IntoElement, ParentElement as _, PathPromptOptions,
|
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::button::Button;
|
||||||
use gpui_native_kit::common::{h_flex, v_flex};
|
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 gpui_native_kit::tooltip::Tooltip;
|
||||||
use settings::{AppearanceMode, EventFetchingStrategy, SettingsStore};
|
use settings::{AppearanceMode, EventFetchingStrategy, SettingsStore};
|
||||||
|
|
||||||
use super::SettingsDialog;
|
|
||||||
use crate::repo::dialogs::grasp_servers::{normalize_server, server_host};
|
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.
|
/// 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();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_folder_selector(
|
pub fn default_folder_selector(default_folder: &Entity<InputState>) -> AnyElement {
|
||||||
default_folder: &Entity<InputState>,
|
|
||||||
weak: WeakEntity<SettingsDialog>,
|
|
||||||
) -> AnyElement {
|
|
||||||
let folder = default_folder.clone();
|
let folder = default_folder.clone();
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
@@ -324,21 +320,15 @@ pub fn default_folder_selector(
|
|||||||
.variant(gpui_native_kit::button::ButtonVariant::Ghost)
|
.variant(gpui_native_kit::button::ButtonVariant::Ghost)
|
||||||
.tooltip(Tooltip::text("Choose folder"))
|
.tooltip(Tooltip::text("Choose folder"))
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
choose_default_folder(&folder, &weak, window, cx);
|
choose_default_folder(&folder, window, cx);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn choose_default_folder(
|
fn choose_default_folder(default_folder: &Entity<InputState>, window: &mut Window, cx: &mut App) {
|
||||||
default_folder: &Entity<InputState>,
|
|
||||||
weak: &WeakEntity<SettingsDialog>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
let handle = window.window_handle();
|
let handle = window.window_handle();
|
||||||
let default_folder = default_folder.clone();
|
let default_folder = default_folder.clone();
|
||||||
let weak = weak.clone();
|
|
||||||
let prompt = cx.prompt_for_paths(PathPromptOptions {
|
let prompt = cx.prompt_for_paths(PathPromptOptions {
|
||||||
files: false,
|
files: false,
|
||||||
directories: true,
|
directories: true,
|
||||||
@@ -359,7 +349,6 @@ fn choose_default_folder(
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
default_folder.update(cx, |input, cx| input.set_value(text, window, cx));
|
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}");
|
log::warn!("failed to apply default folder: {error}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,10 +46,9 @@ impl SettingsDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Render for 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 grasp_input = self.grasp_input.clone();
|
||||||
let default_folder = self.default_folder.clone();
|
let default_folder = self.default_folder.clone();
|
||||||
let weak = cx.entity().downgrade();
|
|
||||||
|
|
||||||
Dialog::new("settings-dialog")
|
Dialog::new("settings-dialog")
|
||||||
.open(true)
|
.open(true)
|
||||||
@@ -95,7 +94,7 @@ impl Render for SettingsDialog {
|
|||||||
cx,
|
cx,
|
||||||
"Default Folder",
|
"Default Folder",
|
||||||
"The folder for newly created repositories.",
|
"The folder for newly created repositories.",
|
||||||
default_folder_selector(&default_folder, weak.clone()),
|
default_folder_selector(&default_folder),
|
||||||
))
|
))
|
||||||
.into_any_element()
|
.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`.
|
/// Replaces the mounted modal with `view`.
|
||||||
pub fn open_global(view: impl Into<AnyView>, cx: &mut App) {
|
pub fn open_global(view: impl Into<AnyView>, cx: &mut App) {
|
||||||
let host = DialogHost::global(cx);
|
let host = DialogHost::global(cx);
|
||||||
@@ -48,12 +57,3 @@ pub fn close_global(cx: &mut App) {
|
|||||||
let host = DialogHost::global(cx);
|
let host = DialogHost::global(cx);
|
||||||
host.update(cx, |host, cx| host.close(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 progress;
|
||||||
pub mod tree;
|
pub mod tree;
|
||||||
pub mod widgets;
|
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;
|
mod helpers;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum SidebarSlot {
|
||||||
|
Inbox,
|
||||||
|
Browse,
|
||||||
|
Search,
|
||||||
|
Header,
|
||||||
|
Repo(usize),
|
||||||
|
Local(usize),
|
||||||
|
}
|
||||||
|
|
||||||
pub struct SidebarView {
|
pub struct SidebarView {
|
||||||
weak: WeakEntity<Self>,
|
weak: WeakEntity<Self>,
|
||||||
dock: WeakEntity<DockState>,
|
dock: WeakEntity<DockState>,
|
||||||
@@ -333,6 +343,10 @@ impl SidebarView {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug_assert_eq!(
|
||||||
|
entries.len(),
|
||||||
|
slots(announcements.len(), local_repos.len()).len()
|
||||||
|
);
|
||||||
entries
|
entries
|
||||||
})
|
})
|
||||||
.on_change(move |index, window, cx| {
|
.on_change(move |index, window, cx| {
|
||||||
@@ -346,25 +360,19 @@ impl SidebarView {
|
|||||||
let Some(index) = index else {
|
let Some(index) = index else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let Some(slot) = slots(self.announcements.len(), self.local_repos.len())
|
||||||
match index {
|
.get(index)
|
||||||
0 => {
|
.copied()
|
||||||
self.open_inbox(window, cx);
|
else {
|
||||||
return;
|
|
||||||
}
|
|
||||||
1 | 2 => {
|
|
||||||
self.open_explore(window, cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(row) = index.checked_sub(4) else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if row < self.announcements.len() {
|
match slot {
|
||||||
let announcement = self.announcements[row].clone();
|
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();
|
let addr = announcement.addr();
|
||||||
crate::repo::actions::open_repo_panel(
|
crate::repo::actions::open_repo_panel(
|
||||||
&self.dock,
|
&self.dock,
|
||||||
@@ -373,14 +381,15 @@ impl SidebarView {
|
|||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let local_ix = row - self.announcements.len();
|
SidebarSlot::Local(ix) => {
|
||||||
if let Some(entry) = self.local_repos.get(local_ix).cloned() {
|
if let Some(entry) = self.local_repos.get(ix).cloned() {
|
||||||
self.open_local_entry(entry, window, cx);
|
self.open_local_entry(entry, window, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A local repository opens as the announced repository when its binding matches one.
|
/// A local repository opens as the announced repository when its binding matches one.
|
||||||
fn open_local_entry(
|
fn open_local_entry(
|
||||||
@@ -456,9 +465,9 @@ impl SidebarView {
|
|||||||
.label("Guide")
|
.label("Guide")
|
||||||
.variant(ButtonVariant::Ghost)
|
.variant(ButtonVariant::Ghost)
|
||||||
.on_click(move |_, window, cx| {
|
.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);
|
this.open_explore(window, cx);
|
||||||
});
|
}));
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.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