chore: refactor the backend (#17)
Reviewed-on: #17
This commit was merged in pull request #17.
This commit is contained in:
@@ -325,8 +325,6 @@ pub struct CommitDiffView {
|
||||
error: Option<SharedString>,
|
||||
/// Changed-files explorer and per-file diff, also used by the new PR panel's compare view.
|
||||
pane: Entity<DiffPane>,
|
||||
/// In-flight tasks, pruned on every push.
|
||||
tasks: Vec<gpui::Task<Result<(), anyhow::Error>>>,
|
||||
}
|
||||
|
||||
impl CommitDiffView {
|
||||
@@ -358,7 +356,6 @@ impl CommitDiffView {
|
||||
loading: true,
|
||||
error: None,
|
||||
pane,
|
||||
tasks: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,42 +368,43 @@ impl CommitDiffView {
|
||||
let worktree = self.worktree.clone();
|
||||
let id = self.commit.id.clone();
|
||||
|
||||
let task = cx.spawn_in(window, async move |this, cx| {
|
||||
let commit = cx
|
||||
.background_spawn({
|
||||
let worktree = worktree.clone();
|
||||
let id = id.clone();
|
||||
async move { signed_git::worktree_commit(&worktree, &id) }
|
||||
})
|
||||
.await;
|
||||
let diff = cx
|
||||
.background_spawn({
|
||||
let worktree = worktree.clone();
|
||||
let id = id.clone();
|
||||
async move { signed_git::worktree_commit_diff(&worktree, &id) }
|
||||
})
|
||||
.await;
|
||||
let task: gpui::Task<Result<(), anyhow::Error>> =
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let commit = cx
|
||||
.background_spawn({
|
||||
let worktree = worktree.clone();
|
||||
let id = id.clone();
|
||||
async move { signed_git::worktree_commit(&worktree, &id) }
|
||||
})
|
||||
.await;
|
||||
let diff = cx
|
||||
.background_spawn({
|
||||
let worktree = worktree.clone();
|
||||
let id = id.clone();
|
||||
async move { signed_git::worktree_commit_diff(&worktree, &id) }
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update_in(cx, |this, _window, cx| {
|
||||
this.loading = false;
|
||||
if let Ok(Some(commit)) = commit {
|
||||
this.commit = commit;
|
||||
}
|
||||
match diff {
|
||||
Ok(diff) => {
|
||||
this.pane.update(cx, |pane, cx| pane.set_diff(diff, cx));
|
||||
this.update_in(cx, |this, _window, cx| {
|
||||
this.loading = false;
|
||||
if let Ok(Some(commit)) = commit {
|
||||
this.commit = commit;
|
||||
}
|
||||
Err(error) => {
|
||||
this.error = Some(error.to_string().into());
|
||||
match diff {
|
||||
Ok(diff) => {
|
||||
this.pane.update(cx, |pane, cx| pane.set_diff(diff, cx));
|
||||
}
|
||||
Err(error) => {
|
||||
this.error = Some(error.to_string().into());
|
||||
}
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
})?;
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
Ok(())
|
||||
});
|
||||
|
||||
self.tasks.push(task);
|
||||
task.detach();
|
||||
}
|
||||
|
||||
/// Header with the commit id, summary, author/time and overall change stats.
|
||||
|
||||
@@ -10,8 +10,8 @@ use gix::Repository;
|
||||
use gpui::prelude::*;
|
||||
use gpui::{
|
||||
Action, Anchor, AnyElement, App, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, PathPromptOptions, Pixels, Render, SharedString, Size, Subscription, Task,
|
||||
WeakEntity, Window, div, px, relative, size, transparent_white,
|
||||
Focusable, PathPromptOptions, Pixels, Render, SharedString, Size, Subscription, WeakEntity,
|
||||
Window, div, px, relative, size, transparent_white,
|
||||
};
|
||||
use gpui_base::{Button as BaseButton, Disableable, Popover};
|
||||
use gpui_component::alert::Alert;
|
||||
@@ -24,7 +24,7 @@ use gpui_component::{
|
||||
ActiveTheme, Colorize, Icon, IconName, Sizable, StyledExt, ThemeStyled,
|
||||
VirtualListScrollHandle, h_flex, v_flex,
|
||||
};
|
||||
use nostr::prelude::{RelayUrl, ToBech32};
|
||||
use nostr::prelude::{RelayUrl, ToBech32, Url};
|
||||
use signed_core::{Announcement, RepoAddr, RepoStatus, filters};
|
||||
use signed_git::{CommitList, FileCommit};
|
||||
use signed_state::{
|
||||
@@ -175,9 +175,6 @@ pub struct RepoDetailView {
|
||||
/// Bumped on every branch/tag switch.
|
||||
/// In-flight loads with an older generation are discarded when they complete.
|
||||
ref_generation: u64,
|
||||
/// In-flight tasks, finished tasks are pruned on every push.
|
||||
/// The vec stays bounded by the number of concurrent loads.
|
||||
tasks: Vec<Task<Result<(), Error>>>,
|
||||
/// Subscriptions keeping the selectors' confirm events alive.
|
||||
_subscriptions: Vec<Subscription>,
|
||||
/// `(path, branch)` ready-suggestions dismissed by the user, per panel.
|
||||
@@ -346,7 +343,6 @@ impl RepoDetailView {
|
||||
push_statuses: Vec::new(),
|
||||
pending_upstream: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
tasks: Vec::new(),
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
@@ -363,7 +359,7 @@ impl RepoDetailView {
|
||||
// Local repositories live on disk at their scan path.
|
||||
// No clone step or network refresh applies here.
|
||||
if let Some(local_path) = self.local_path.clone() {
|
||||
let task = cx.spawn_in(window, async move |this, cx| {
|
||||
let task: gpui::Task<Result<(), Error>> = cx.spawn_in(window, async move |this, cx| {
|
||||
let data = cx
|
||||
.background_spawn(async move {
|
||||
let repo = gix::open(&local_path)?;
|
||||
@@ -383,7 +379,7 @@ impl RepoDetailView {
|
||||
Ok(())
|
||||
});
|
||||
|
||||
self.tasks.push(task);
|
||||
task.detach();
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -394,7 +390,7 @@ impl RepoDetailView {
|
||||
|
||||
let cache = GitStore::global(cx).cache().clone();
|
||||
let addr = initial.addr();
|
||||
let clone_urls: Vec<String> = initial.clone.iter().map(ToString::to_string).collect();
|
||||
let clone_urls: Vec<Url> = initial.clone.clone();
|
||||
|
||||
// Captured before the loads start.
|
||||
// A branch/tag switch bumps the generation, discarding the refresh below.
|
||||
@@ -411,7 +407,7 @@ impl RepoDetailView {
|
||||
})
|
||||
};
|
||||
|
||||
let task = cx.spawn_in(window, async move |this, cx| {
|
||||
let task: gpui::Task<Result<(), Error>> = cx.spawn_in(window, async move |this, cx| {
|
||||
let disk = disk.await;
|
||||
let had_clone = matches!(&disk, Ok(Some(_)));
|
||||
|
||||
@@ -531,7 +527,7 @@ impl RepoDetailView {
|
||||
Ok(())
|
||||
});
|
||||
|
||||
self.tasks.push(task);
|
||||
task.detach();
|
||||
}
|
||||
|
||||
/// Apply the loaded repository data.
|
||||
@@ -619,7 +615,7 @@ impl RepoDetailView {
|
||||
prompt: Some("Clone".into()),
|
||||
});
|
||||
|
||||
let task = cx.spawn_in(window, async move |this, cx| {
|
||||
let task: gpui::Task<Result<(), Error>> = cx.spawn_in(window, async move |this, cx| {
|
||||
// `Ok(Ok(Some(paths)))` means the user picked a folder.
|
||||
// A cancel or picker failure resolves to anything else.
|
||||
let picked = match prompt.await {
|
||||
@@ -649,7 +645,7 @@ impl RepoDetailView {
|
||||
Ok(())
|
||||
});
|
||||
|
||||
self.tasks.push(task);
|
||||
task.detach();
|
||||
}
|
||||
|
||||
/// Preview the file at `path`, relative to the worktree root.
|
||||
@@ -703,7 +699,7 @@ impl RepoDetailView {
|
||||
self.load_commit(&path, cx);
|
||||
let generation = self.ref_generation;
|
||||
|
||||
let task = cx.spawn_in(window, async move |this, cx| {
|
||||
let task: gpui::Task<Result<(), Error>> = cx.spawn_in(window, async move |this, cx| {
|
||||
let path_for_read = path.clone();
|
||||
let content = cx
|
||||
.background_spawn(async move {
|
||||
@@ -772,7 +768,7 @@ impl RepoDetailView {
|
||||
Ok(())
|
||||
});
|
||||
|
||||
self.tasks.push(task);
|
||||
task.detach();
|
||||
}
|
||||
|
||||
/// Queue `path` for the per-file commit query.
|
||||
@@ -804,7 +800,7 @@ impl RepoDetailView {
|
||||
let paths = std::mem::take(&mut self.pending_commits);
|
||||
let generation = self.ref_generation;
|
||||
|
||||
let task = cx.spawn(async move |this, cx| {
|
||||
let task: gpui::Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
|
||||
let rels: Vec<PathBuf> = paths.iter().map(PathBuf::from).collect();
|
||||
let result = cx
|
||||
.background_spawn(
|
||||
@@ -834,7 +830,7 @@ impl RepoDetailView {
|
||||
Ok(())
|
||||
});
|
||||
|
||||
self.tasks.push(task);
|
||||
task.detach();
|
||||
}
|
||||
|
||||
/// Walk all commits reachable from HEAD on a background task.
|
||||
@@ -852,7 +848,7 @@ impl RepoDetailView {
|
||||
self.loading_all_commits = true;
|
||||
let generation = self.ref_generation;
|
||||
|
||||
let task = cx.spawn(async move |this, cx| {
|
||||
let task: gpui::Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
|
||||
let result = cx
|
||||
.background_spawn(async move { signed_git::worktree_all_commits(&worktree) })
|
||||
.await;
|
||||
@@ -876,7 +872,7 @@ impl RepoDetailView {
|
||||
Ok(())
|
||||
});
|
||||
|
||||
self.tasks.push(task);
|
||||
task.detach();
|
||||
}
|
||||
|
||||
/// Open a new panel showing the diff of `commit_id`.
|
||||
@@ -909,8 +905,9 @@ impl RepoDetailView {
|
||||
self.error = None;
|
||||
cx.notify();
|
||||
|
||||
self.tasks
|
||||
.push(store.update(cx, |store, cx| store.push_repository(cx)));
|
||||
store
|
||||
.update(cx, |store, cx| store.push_repository(cx))
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Push the unpushed commits of the local checkout at `path`.
|
||||
@@ -931,7 +928,7 @@ impl RepoDetailView {
|
||||
self.error = None;
|
||||
cx.notify();
|
||||
|
||||
let task = cx.spawn_in(window, async move |this, cx| {
|
||||
let task: gpui::Task<Result<(), Error>> = cx.spawn_in(window, async move |this, cx| {
|
||||
// The store owns the push, its busy flag and error reporting.
|
||||
let push = this.update_in(cx, |_this, _window, cx| {
|
||||
store.update(cx, |store, cx| store.push_checkout(path.clone(), cx))
|
||||
@@ -948,7 +945,7 @@ impl RepoDetailView {
|
||||
Ok(())
|
||||
});
|
||||
|
||||
self.tasks.push(task);
|
||||
task.detach();
|
||||
}
|
||||
|
||||
/// Delete the repository from nostr, announcement, state and activity.
|
||||
@@ -956,8 +953,9 @@ impl RepoDetailView {
|
||||
let Some(store) = self.store.clone() else {
|
||||
return;
|
||||
};
|
||||
self.tasks
|
||||
.push(store.update(cx, |store, cx| store.delete_repository(cx)));
|
||||
store
|
||||
.update(cx, |store, cx| store.delete_repository(cx))
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Open the issues list panel in the dock area.
|
||||
@@ -1025,7 +1023,7 @@ impl RepoDetailView {
|
||||
});
|
||||
self.pending_upstream = Some(addr);
|
||||
|
||||
let task = cx.spawn_in(window, async move |this, cx| {
|
||||
let task: gpui::Task<Result<(), Error>> = cx.spawn_in(window, async move |this, cx| {
|
||||
for _ in 0..60 {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(250))
|
||||
@@ -1060,7 +1058,7 @@ impl RepoDetailView {
|
||||
Ok(())
|
||||
});
|
||||
|
||||
self.tasks.push(task);
|
||||
task.detach();
|
||||
}
|
||||
|
||||
/// Check out `name`, a branch or tag picked in the header.
|
||||
@@ -1101,7 +1099,7 @@ impl RepoDetailView {
|
||||
cx.notify();
|
||||
|
||||
let checkout_name = name.clone();
|
||||
let task = cx.spawn_in(window, async move |this, cx| {
|
||||
let task: gpui::Task<Result<(), Error>> = cx.spawn_in(window, async move |this, cx| {
|
||||
let result = cx
|
||||
.background_spawn(async move {
|
||||
match kind {
|
||||
@@ -1131,7 +1129,7 @@ impl RepoDetailView {
|
||||
Ok(())
|
||||
});
|
||||
|
||||
self.tasks.push(task);
|
||||
task.detach();
|
||||
}
|
||||
|
||||
/// Restore a selector to `previous`, or clear it after a failed switch.
|
||||
@@ -1157,7 +1155,7 @@ impl RepoDetailView {
|
||||
return;
|
||||
};
|
||||
|
||||
let task = cx.spawn(async move |this, cx| {
|
||||
let task: gpui::Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
|
||||
let result = cx
|
||||
.background_spawn(async move {
|
||||
let snapshot = signed_git::worktree_snapshot(&worktree)?;
|
||||
@@ -1218,7 +1216,7 @@ impl RepoDetailView {
|
||||
Ok(())
|
||||
});
|
||||
|
||||
self.tasks.push(task);
|
||||
task.detach();
|
||||
}
|
||||
|
||||
/// Refresh the file explorer, previews and commit list after the mirror
|
||||
@@ -1233,7 +1231,7 @@ impl RepoDetailView {
|
||||
return;
|
||||
};
|
||||
|
||||
let task = cx.spawn(async move |this, cx| {
|
||||
let task: gpui::Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
|
||||
let result = cx
|
||||
.background_spawn(async move {
|
||||
let snapshot = signed_git::worktree_snapshot(&worktree)?;
|
||||
@@ -1314,7 +1312,7 @@ impl RepoDetailView {
|
||||
Ok(())
|
||||
});
|
||||
|
||||
self.tasks.push(task);
|
||||
task.detach();
|
||||
}
|
||||
|
||||
/// Drop the cached preview, editor and commit state of `path`.
|
||||
|
||||
@@ -6,8 +6,7 @@ use dock::{BasePanel, DockArea, Panel, PanelEvent, add_center_panel, panel_handl
|
||||
use gpui::prelude::*;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, PathPromptOptions,
|
||||
Pixels, Render, SharedString, Size, Subscription, Task, WeakEntity, Window, div, px, relative,
|
||||
size,
|
||||
Pixels, Render, SharedString, Size, Subscription, WeakEntity, Window, div, px, relative, size,
|
||||
};
|
||||
use gpui_base::{Button as BaseButton, StyledExt};
|
||||
use gpui_component::button::{Button, ButtonVariants};
|
||||
@@ -22,10 +21,10 @@ use gpui_component::{
|
||||
v_virtual_list,
|
||||
};
|
||||
use nostr::prelude::*;
|
||||
use signed_core::{Announcement, RepoAddr};
|
||||
use signed_core::{Announcement, RepoAddr, fork_candidates};
|
||||
use signed_git::{
|
||||
delete_refs_with_prefix, fetch_repo_refs, format_patch_between, merge_base, refs_with_prefix,
|
||||
sanitize_path_component, worktree_commit_range_commits, worktree_commit_range_diff,
|
||||
delete_refs_with_prefix, fetch_repo_refs, fork_namespace, merge_base, refs_with_prefix,
|
||||
worktree_commit_range_commits, worktree_commit_range_diff,
|
||||
};
|
||||
use signed_state::{Backend, CheckoutsStore, GitStore, RepoListStore, RepoStore};
|
||||
use signed_ui::{CountBadge, placeholder};
|
||||
@@ -79,7 +78,6 @@ pub struct NewPullRequestView {
|
||||
scroll_handle: VirtualListScrollHandle,
|
||||
item_sizes: Rc<Vec<Size<Pixels>>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
tasks: Vec<Task<Result<(), anyhow::Error>>>,
|
||||
}
|
||||
|
||||
/// A fork-backed compare.
|
||||
@@ -104,36 +102,6 @@ impl ForkCompare {
|
||||
}
|
||||
}
|
||||
|
||||
/// The refs namespace of a fork's import in the target mirror.
|
||||
fn fork_namespace(announcement: &Announcement) -> String {
|
||||
format!(
|
||||
"{}/{}",
|
||||
announcement.owner.to_hex(),
|
||||
sanitize_path_component(&announcement.id)
|
||||
)
|
||||
}
|
||||
|
||||
/// The announced forks of `base` a New PR compare can be built from.
|
||||
fn fork_candidates<'a>(
|
||||
announcements: &'a [Announcement],
|
||||
base: &RepoAddr,
|
||||
base_euc: Option<&str>,
|
||||
user: Option<PublicKey>,
|
||||
) -> Vec<&'a Announcement> {
|
||||
let (mut own, mut others) = (Vec::new(), Vec::new());
|
||||
for announcement in announcements {
|
||||
if announcement.clone.is_empty() || !announcement.is_fork_of(base, base_euc) {
|
||||
continue;
|
||||
}
|
||||
if Some(announcement.owner) == user {
|
||||
own.push(announcement);
|
||||
} else {
|
||||
others.push(announcement);
|
||||
}
|
||||
}
|
||||
own.into_iter().chain(others).collect()
|
||||
}
|
||||
|
||||
/// The display name of an announcement.
|
||||
///
|
||||
/// Its human-readable name, falling back to the repository id.
|
||||
@@ -363,7 +331,6 @@ impl NewPullRequestView {
|
||||
scroll_handle: VirtualListScrollHandle::new(),
|
||||
item_sizes: Rc::new(Vec::new()),
|
||||
_subscriptions: subscriptions,
|
||||
tasks: Vec::new(),
|
||||
};
|
||||
|
||||
// Prefill with the store's freshest associated checkout, no folder dialog.
|
||||
@@ -426,25 +393,26 @@ impl NewPullRequestView {
|
||||
prompt: Some("Choose local checkout".into()),
|
||||
});
|
||||
|
||||
let task = cx.spawn_in(window, async move |this, cx| {
|
||||
// `Ok(Ok(Some(paths)))` means the user picked a folder.
|
||||
// A cancel or picker failure resolves to anything else.
|
||||
let picked = match prompt.await {
|
||||
Ok(Ok(Some(mut paths))) => paths.pop(),
|
||||
_ => None,
|
||||
};
|
||||
let task: gpui::Task<Result<(), anyhow::Error>> =
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
// `Ok(Ok(Some(paths)))` means the user picked a folder.
|
||||
// A cancel or picker failure resolves to anything else.
|
||||
let picked = match prompt.await {
|
||||
Ok(Ok(Some(mut paths))) => paths.pop(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let Some(path) = picked else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(path) = picked else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.apply_folder_path(path, window, cx);
|
||||
})?;
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.apply_folder_path(path, window, cx);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
self.tasks.push(task);
|
||||
Ok(())
|
||||
});
|
||||
task.detach();
|
||||
}
|
||||
|
||||
/// Apply `path` as the local checkout, no picker.
|
||||
@@ -453,28 +421,29 @@ impl NewPullRequestView {
|
||||
fn apply_folder_path(&mut self, path: PathBuf, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let path = path.to_string_lossy().to_string();
|
||||
|
||||
let task = cx.spawn_in(window, async move |this, cx| {
|
||||
// Branches and the current branch are read off the UI thread.
|
||||
let info = cx
|
||||
.background_spawn({
|
||||
let path = path.clone();
|
||||
async move {
|
||||
let repo = gix::open(Path::new(&path)).ok()?;
|
||||
let branches =
|
||||
signed_git::worktree_branches(Path::new(&path)).unwrap_or_default();
|
||||
let current = signed_git::current_branch(&repo).ok().flatten();
|
||||
Some((branches, current))
|
||||
}
|
||||
})
|
||||
.await;
|
||||
let task: gpui::Task<Result<(), anyhow::Error>> =
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
// Branches and the current branch are read off the UI thread.
|
||||
let info = cx
|
||||
.background_spawn({
|
||||
let path = path.clone();
|
||||
async move {
|
||||
let repo = gix::open(Path::new(&path)).ok()?;
|
||||
let branches =
|
||||
signed_git::worktree_branches(Path::new(&path)).unwrap_or_default();
|
||||
let current = signed_git::current_branch(&repo).ok().flatten();
|
||||
Some((branches, current))
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.apply_checkout(path, info, window, cx);
|
||||
})?;
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.apply_checkout(path, info, window, cx);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
self.tasks.push(task);
|
||||
Ok(())
|
||||
});
|
||||
task.detach();
|
||||
}
|
||||
|
||||
/// Apply a picked checkout, filling the selectors and loading the compare.
|
||||
@@ -592,14 +561,14 @@ impl NewPullRequestView {
|
||||
let cache = GitStore::global(cx).cache().clone();
|
||||
let mirror_path = cache.repo_path(&base);
|
||||
let namespace = fork_namespace(&announcement);
|
||||
let clone_urls: Vec<String> = announcement.clone.iter().map(ToString::to_string).collect();
|
||||
let clone_urls = announcement.clone.clone();
|
||||
|
||||
let base_clone_urls: Vec<String> = self
|
||||
let base_clone_urls: Vec<Url> = self
|
||||
.store
|
||||
.read(cx)
|
||||
.announcement
|
||||
.as_ref()
|
||||
.map(|a| a.clone.iter().map(ToString::to_string).collect())
|
||||
.map(|a| a.clone.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Keep the current compare and base when the fork is already applied.
|
||||
@@ -615,88 +584,89 @@ impl NewPullRequestView {
|
||||
self.error = None;
|
||||
cx.notify();
|
||||
|
||||
let task = cx.spawn_in(window, async move |this, cx| {
|
||||
// The fork and base must share history for a merge-base to exist.
|
||||
// The target's mirror is the object store both sides land in.
|
||||
// `ensure_clone` fetches `origin` when the mirror already exists.
|
||||
let result = cx
|
||||
.background_spawn({
|
||||
let cache = cache.clone();
|
||||
let base = base.clone();
|
||||
let base_clone_urls = base_clone_urls.clone();
|
||||
let namespace = namespace.clone();
|
||||
let clone_urls = clone_urls.clone();
|
||||
let mirror_path = mirror_path.clone();
|
||||
async move {
|
||||
// The fork and base must share history for a merge-base to exist.
|
||||
// The target's mirror is the object store both sides land in.
|
||||
// `ensure_clone` fetches `origin` when the mirror already exists.
|
||||
cache.ensure_clone(&base, &base_clone_urls)?;
|
||||
let task: gpui::Task<Result<(), anyhow::Error>> =
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
// The fork and base must share history for a merge-base to exist.
|
||||
// The target's mirror is the object store both sides land in.
|
||||
// `ensure_clone` fetches `origin` when the mirror already exists.
|
||||
let result = cx
|
||||
.background_spawn({
|
||||
let cache = cache.clone();
|
||||
let base = base.clone();
|
||||
let base_clone_urls = base_clone_urls.clone();
|
||||
let namespace = namespace.clone();
|
||||
let clone_urls = clone_urls.clone();
|
||||
let mirror_path = mirror_path.clone();
|
||||
async move {
|
||||
// The fork and base must share history for a merge-base to exist.
|
||||
// The target's mirror is the object store both sides land in.
|
||||
// `ensure_clone` fetches `origin` when the mirror already exists.
|
||||
cache.ensure_clone(&base, &base_clone_urls)?;
|
||||
|
||||
// Prune stale imports of any fork.
|
||||
// Then import this fork's heads under its namespace.
|
||||
delete_refs_with_prefix(&mirror_path, "refs/fork")?;
|
||||
// Prune stale imports of any fork.
|
||||
// Then import this fork's heads under its namespace.
|
||||
delete_refs_with_prefix(&mirror_path, "refs/fork")?;
|
||||
|
||||
fetch_repo_refs(
|
||||
&mirror_path,
|
||||
&clone_urls,
|
||||
&format!("+refs/heads/*:refs/fork/{namespace}/*"),
|
||||
)?;
|
||||
fetch_repo_refs(
|
||||
&mirror_path,
|
||||
&clone_urls,
|
||||
&format!("+refs/heads/*:refs/fork/{namespace}/*"),
|
||||
)?;
|
||||
|
||||
// Both branch lists are short names, sorted like the checkout's.
|
||||
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
|
||||
};
|
||||
// Both branch lists are short names, sorted like the checkout's.
|
||||
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(
|
||||
refs_with_prefix(&mirror_path, "refs/remotes/origin")?,
|
||||
"refs/remotes/origin",
|
||||
);
|
||||
let base_branches = strip(
|
||||
refs_with_prefix(&mirror_path, "refs/remotes/origin")?,
|
||||
"refs/remotes/origin",
|
||||
);
|
||||
|
||||
let compare_branches = strip(
|
||||
refs_with_prefix(&mirror_path, &format!("refs/fork/{namespace}"))?,
|
||||
&format!("refs/fork/{namespace}"),
|
||||
);
|
||||
let compare_branches = strip(
|
||||
refs_with_prefix(&mirror_path, &format!("refs/fork/{namespace}"))?,
|
||||
&format!("refs/fork/{namespace}"),
|
||||
);
|
||||
|
||||
Ok::<_, anyhow::Error>((base_branches, compare_branches))
|
||||
Ok::<_, anyhow::Error>((base_branches, compare_branches))
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
// A source switch mid-flight discards the stale result.
|
||||
// E.g. the user picked a folder while the fork was fetching.
|
||||
let applied = this.fork.as_ref().map(|fork| fork.announcement.addr());
|
||||
if applied != expected_fork {
|
||||
this.loading = false;
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
// A source switch mid-flight discards the stale result.
|
||||
// E.g. the user picked a folder while the fork was fetching.
|
||||
let applied = this.fork.as_ref().map(|fork| fork.announcement.addr());
|
||||
if applied != expected_fork {
|
||||
this.loading = false;
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
this.apply_fork(
|
||||
announcement,
|
||||
mirror_path,
|
||||
namespace,
|
||||
result,
|
||||
keep_base,
|
||||
keep_compare,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
|
||||
this.apply_fork(
|
||||
announcement,
|
||||
mirror_path,
|
||||
namespace,
|
||||
result,
|
||||
keep_base,
|
||||
keep_compare,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
self.tasks.push(task);
|
||||
Ok(())
|
||||
});
|
||||
task.detach();
|
||||
}
|
||||
|
||||
/// Apply an imported fork, filling the selectors and loading the compare.
|
||||
@@ -834,66 +804,68 @@ impl NewPullRequestView {
|
||||
return;
|
||||
}
|
||||
|
||||
let task = cx.spawn_in(window, async move |this, cx| {
|
||||
let result = cx
|
||||
.background_spawn({
|
||||
let repo_path = repo_path.clone();
|
||||
let base = base.clone();
|
||||
let compare = compare.clone();
|
||||
let base_name = base_name.clone();
|
||||
let compare_name = compare_name.clone();
|
||||
async move {
|
||||
let merge_base = merge_base(Path::new(&repo_path), &base, &compare)?
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"{base_name} and {compare_name} share no common ancestor"
|
||||
)
|
||||
})?;
|
||||
let commits = worktree_commit_range_commits(
|
||||
Path::new(&repo_path),
|
||||
&merge_base,
|
||||
&compare,
|
||||
)?;
|
||||
let diff = worktree_commit_range_diff(
|
||||
Path::new(&repo_path),
|
||||
&merge_base,
|
||||
&compare,
|
||||
)?;
|
||||
Ok::<_, anyhow::Error>((merge_base, commits, diff))
|
||||
let task: gpui::Task<Result<(), anyhow::Error>> =
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let result = cx
|
||||
.background_spawn({
|
||||
let repo_path = repo_path.clone();
|
||||
let base = base.clone();
|
||||
let compare = compare.clone();
|
||||
let base_name = base_name.clone();
|
||||
let compare_name = compare_name.clone();
|
||||
async move {
|
||||
let merge_base = merge_base(Path::new(&repo_path), &base, &compare)?
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"{base_name} and {compare_name} share no common ancestor"
|
||||
)
|
||||
})?;
|
||||
let commits = worktree_commit_range_commits(
|
||||
Path::new(&repo_path),
|
||||
&merge_base,
|
||||
&compare,
|
||||
)?;
|
||||
let diff = worktree_commit_range_diff(
|
||||
Path::new(&repo_path),
|
||||
&merge_base,
|
||||
&compare,
|
||||
)?;
|
||||
Ok::<_, anyhow::Error>((merge_base, commits, diff))
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update_in(cx, |this, _window, cx| {
|
||||
// A stale result, branches changed mid-flight, must not clobber a newer compare.
|
||||
if generation != this.compare_generation {
|
||||
return;
|
||||
}
|
||||
})
|
||||
.await;
|
||||
this.loading = false;
|
||||
|
||||
this.update_in(cx, |this, _window, cx| {
|
||||
// A stale result, branches changed mid-flight, must not clobber a newer compare.
|
||||
if generation != this.compare_generation {
|
||||
return;
|
||||
}
|
||||
this.loading = false;
|
||||
|
||||
match result {
|
||||
Ok((merge_base, commits, diff)) => {
|
||||
this.merge_base = Some(merge_base);
|
||||
let count = commits.len();
|
||||
this.item_sizes = Rc::new(vec![size(px(0.), px(COMMIT_ROW_HEIGHT)); count]);
|
||||
this.commits = Some(commits);
|
||||
this.pane.update(cx, |pane, cx| pane.set_diff(diff, cx));
|
||||
match result {
|
||||
Ok((merge_base, commits, diff)) => {
|
||||
this.merge_base = Some(merge_base);
|
||||
let count = commits.len();
|
||||
this.item_sizes =
|
||||
Rc::new(vec![size(px(0.), px(COMMIT_ROW_HEIGHT)); count]);
|
||||
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());
|
||||
}
|
||||
}
|
||||
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();
|
||||
})?;
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
Ok(())
|
||||
});
|
||||
|
||||
self.tasks.push(task);
|
||||
task.detach();
|
||||
}
|
||||
|
||||
/// Publish the pull request.
|
||||
@@ -927,76 +899,55 @@ impl NewPullRequestView {
|
||||
self.error = None;
|
||||
cx.notify();
|
||||
|
||||
let task = cx.spawn_in(window, async move |this, cx| {
|
||||
// Regenerate the series at submit time.
|
||||
// The published patch covers the current tip of the compare branch.
|
||||
let patch = cx
|
||||
.background_spawn({
|
||||
let repo_path = repo_path.clone();
|
||||
let merge_base = merge_base.clone();
|
||||
let compare_ref = compare_ref.clone();
|
||||
async move {
|
||||
format_patch_between(Path::new(&repo_path), &merge_base, &compare_ref)
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let patch = match patch {
|
||||
Ok(patch) if !patch.is_empty() => patch,
|
||||
Ok(_) => {
|
||||
this.update_in(cx, |this, _window, cx| {
|
||||
this.submitting = false;
|
||||
this.error = Some("No commits between the branches to propose".into());
|
||||
cx.notify();
|
||||
})?;
|
||||
return Ok(());
|
||||
}
|
||||
Err(error) => {
|
||||
this.update_in(cx, |this, _window, cx| {
|
||||
this.submitting = false;
|
||||
this.error = Some(format!("Failed to generate the patch: {error}").into());
|
||||
cx.notify();
|
||||
})?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.submitting = false;
|
||||
|
||||
store.update(cx, |store, cx| {
|
||||
store.open_pull_request(
|
||||
let task: gpui::Task<Result<(), anyhow::Error>> =
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
// Regenerate the series at submit time.
|
||||
// The published patch covers the current tip of the compare branch.
|
||||
let publish = store.update(cx, |store, cx| {
|
||||
store.open_pull_request_from_refs(
|
||||
repo_path,
|
||||
merge_base,
|
||||
compare_ref,
|
||||
(!subject.is_empty()).then_some(subject),
|
||||
description,
|
||||
Some(branch_name),
|
||||
patch,
|
||||
false,
|
||||
Some(merge_base),
|
||||
Some(repo_path),
|
||||
cx,
|
||||
);
|
||||
)
|
||||
});
|
||||
|
||||
// Close the panel once the publish is underway.
|
||||
cx.defer_in(window, {
|
||||
let dock_area = dock_area.clone();
|
||||
let entity = entity.clone();
|
||||
move |_, window, cx| {
|
||||
if let Some(dock_area) = dock_area.upgrade() {
|
||||
dock_area.update(cx, |dock, cx| {
|
||||
dock.remove_panel(entity, window, cx);
|
||||
});
|
||||
if let Err(error) = publish.await {
|
||||
this.update_in(cx, |this, _window, cx| {
|
||||
this.submitting = false;
|
||||
this.error = Some(error.to_string().into());
|
||||
cx.notify();
|
||||
})?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.submitting = false;
|
||||
|
||||
// Close the panel once the publish is underway.
|
||||
cx.defer_in(window, {
|
||||
let dock_area = dock_area.clone();
|
||||
let entity = entity.clone();
|
||||
move |_, window, cx| {
|
||||
if let Some(dock_area) = dock_area.upgrade() {
|
||||
dock_area.update(cx, |dock, cx| {
|
||||
dock.remove_panel(entity, window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
})?;
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
Ok(())
|
||||
});
|
||||
|
||||
self.tasks.push(task);
|
||||
task.detach();
|
||||
}
|
||||
|
||||
/// Open the diff of `commit_id`, from the Commits tab, in a new panel.
|
||||
@@ -1459,140 +1410,3 @@ impl Render for NewPullRequestView {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use nostr::prelude::*;
|
||||
use signed_core::repo_addr;
|
||||
|
||||
use super::*;
|
||||
|
||||
const OWNER_KEYS: [&str; 3] = [
|
||||
"0000000000000000000000000000000000000000000000000000000000000001",
|
||||
"0000000000000000000000000000000000000000000000000000000000000002",
|
||||
"0000000000000000000000000000000000000000000000000000000000000003",
|
||||
];
|
||||
|
||||
/// Build a signed kind-30617 event for `owner` with the given tags.
|
||||
fn announcement_event(owner: &str, tags: &[&[&str]]) -> Event {
|
||||
let keys = Keys::new(SecretKey::from_hex(owner).expect("valid secret key"));
|
||||
let tags: Vec<Tag> = tags
|
||||
.iter()
|
||||
.map(|t| Tag::parse(t.to_vec()).expect("valid tag"))
|
||||
.collect();
|
||||
EventBuilder::new(Kind::GitRepoAnnouncement, "")
|
||||
.tags(tags)
|
||||
.finalize(&keys)
|
||||
.expect("signed event")
|
||||
}
|
||||
|
||||
fn announcements(owner_ix: usize, tags: &[&[&str]]) -> Vec<Announcement> {
|
||||
vec![
|
||||
Announcement::from_event(&announcement_event(OWNER_KEYS[owner_ix], tags))
|
||||
.expect("parses"),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fork_candidates_orders_own_forks_first() {
|
||||
let euc = "aa231c4c6a5777dc89b42207b499891a344add5c";
|
||||
let clone = "https://grasp.example/npub1x/my-fork.git";
|
||||
|
||||
let base_addr = repo_addr(
|
||||
PublicKey::from_hex(OWNER_KEYS[0]).expect("pubkey"),
|
||||
"upstream",
|
||||
);
|
||||
// Newest first, as RepoListStore keeps them.
|
||||
// Unrelated repo, the user's fork with the shared EUC, another fork with a `u` tag.
|
||||
let all = vec![
|
||||
announcements(
|
||||
2,
|
||||
&[
|
||||
&["d", "other-project"],
|
||||
&["r", "bb231c4c6a5777dc89b42207b499891a344add5c", "euc"],
|
||||
],
|
||||
)
|
||||
.pop()
|
||||
.unwrap(),
|
||||
announcements(
|
||||
1,
|
||||
&[&["d", "my-fork"], &["r", euc, "euc"], &["clone", clone]],
|
||||
)
|
||||
.pop()
|
||||
.unwrap(),
|
||||
announcements(
|
||||
2,
|
||||
&[
|
||||
&["d", "their-fork"],
|
||||
&["u", &base_addr.to_string()],
|
||||
&["clone", clone],
|
||||
],
|
||||
)
|
||||
.pop()
|
||||
.unwrap(),
|
||||
];
|
||||
|
||||
let user = PublicKey::from_hex(OWNER_KEYS[1]).expect("pubkey");
|
||||
let forks = fork_candidates(&all, &base_addr, Some(euc), Some(user));
|
||||
|
||||
// The user's fork comes first, then the other author's.
|
||||
let ids: Vec<&str> = forks.iter().map(|a| a.id.as_str()).collect();
|
||||
assert_eq!(ids, vec!["my-fork", "their-fork"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fork_candidates_excludes_base_unrelated_and_unfetchable() {
|
||||
let euc = "aa231c4c6a5777dc89b42207b499891a344add5c";
|
||||
let base_owner = PublicKey::from_hex(OWNER_KEYS[0]).expect("pubkey");
|
||||
let base_addr = repo_addr(base_owner, "upstream");
|
||||
|
||||
let mut all = vec![
|
||||
announcements(0, &[&["d", "upstream"], &["r", euc, "euc"]])
|
||||
.pop()
|
||||
.unwrap(),
|
||||
announcements(1, &[&["d", "no-clone-fork"], &["r", euc, "euc"]])
|
||||
.pop()
|
||||
.unwrap(),
|
||||
announcements(
|
||||
2,
|
||||
&[
|
||||
&["d", "other"],
|
||||
&["r", "cc231c4c6a5777dc89b42207b499891a344add5c", "euc"],
|
||||
],
|
||||
)
|
||||
.pop()
|
||||
.unwrap(),
|
||||
announcements(
|
||||
2,
|
||||
&[
|
||||
&["d", "mirror"],
|
||||
&["r", euc, "euc"],
|
||||
&["clone", "https://grasp.example/x/mirror.git"],
|
||||
],
|
||||
)
|
||||
.pop()
|
||||
.unwrap(),
|
||||
];
|
||||
|
||||
let forks = fork_candidates(&all, &base_addr, Some(euc), Some(base_owner));
|
||||
assert_eq!(forks.len(), 1);
|
||||
assert_eq!(forks[0].id, "mirror");
|
||||
|
||||
// Without a base EUC only `u`-tag forks match.
|
||||
all.push(
|
||||
announcements(
|
||||
2,
|
||||
&[
|
||||
&["d", "u-fork"],
|
||||
&["u", &base_addr.to_string()],
|
||||
&["clone", "https://grasp.example/x/u-fork.git"],
|
||||
],
|
||||
)
|
||||
.pop()
|
||||
.unwrap(),
|
||||
);
|
||||
let forks = fork_candidates(&all, &base_addr, None, Some(base_owner));
|
||||
let ids: Vec<&str> = forks.iter().map(|a| a.id.as_str()).collect();
|
||||
assert_eq!(ids, vec!["u-fork"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use dock::{BasePanel, DockArea, DockPlacement, Panel, PanelEvent, panel_handle};
|
||||
use gpui::prelude::*;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render,
|
||||
SharedString, Size, Task, WeakEntity, Window, div, px, relative, size,
|
||||
SharedString, Size, WeakEntity, Window, div, px, relative, size,
|
||||
};
|
||||
use gpui_component::button::{Button, ButtonVariants};
|
||||
use gpui_component::clipboard::Clipboard;
|
||||
@@ -20,8 +20,11 @@ use gpui_component::{
|
||||
ActiveTheme, Sizable, StyledExt, VirtualListScrollHandle, WindowExt, h_flex, v_flex,
|
||||
v_virtual_list,
|
||||
};
|
||||
use nostr::prelude::{Event, EventId, Kind, Nip34Tag};
|
||||
use signed_core::{activity_subject, pull_request_patch};
|
||||
use nostr::prelude::{Event, EventId, Kind};
|
||||
use signed_core::{
|
||||
activity_subject, branch_name_of, clone_urls_of, current_commit_of, latest_update,
|
||||
merge_base_of, pull_request_patch,
|
||||
};
|
||||
use signed_git::{FileCommit, patch_commits, patch_diffs};
|
||||
use signed_state::{Backend, GitStore, ProfileStore, RepoStore};
|
||||
use signed_ui::{CountBadge, UserAvatar, placeholder, status_badge};
|
||||
@@ -66,8 +69,6 @@ pub struct PullRequestDetailView {
|
||||
commit_item_sizes: Rc<Vec<Size<Pixels>>>,
|
||||
/// Virtual list state of the commits tab.
|
||||
commit_scroll_handle: VirtualListScrollHandle,
|
||||
/// In-flight tasks, finished tasks are pruned on every push.
|
||||
tasks: Vec<Task<Result<(), anyhow::Error>>>,
|
||||
}
|
||||
|
||||
impl PullRequestDetailView {
|
||||
@@ -106,7 +107,6 @@ impl PullRequestDetailView {
|
||||
pane,
|
||||
commit_item_sizes: Rc::new(Vec::new()),
|
||||
commit_scroll_handle: VirtualListScrollHandle::new(),
|
||||
tasks: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,12 +142,8 @@ impl PullRequestDetailView {
|
||||
.and_then(merge_base_of)
|
||||
.or_else(|| merge_base_of(root));
|
||||
|
||||
let clone_urls = clone_urls_of(root).or_else(|| {
|
||||
store
|
||||
.announcement
|
||||
.as_ref()
|
||||
.map(|a| a.clone.iter().map(ToString::to_string).collect())
|
||||
});
|
||||
let clone_urls = clone_urls_of(root)
|
||||
.or_else(|| store.announcement.as_ref().map(|a| a.clone.clone()));
|
||||
|
||||
(
|
||||
root.content.clone(),
|
||||
@@ -162,101 +158,103 @@ impl PullRequestDetailView {
|
||||
|
||||
self.description = description.into();
|
||||
|
||||
let task = cx.spawn_in(window, async move |this, cx| {
|
||||
let nostr_diff = cx
|
||||
.background_spawn({
|
||||
let patch = patch.clone();
|
||||
async move { patch_diffs(&patch) }
|
||||
})
|
||||
.await;
|
||||
|
||||
let nostr_commits = cx
|
||||
.background_spawn({
|
||||
let patch = patch.clone();
|
||||
async move { patch_commits(&patch) }
|
||||
})
|
||||
.await;
|
||||
|
||||
// PRs without patch events, e.g. published by ngit, carry their changes in git.
|
||||
// Fetch the clone and diff the `merge-base..tip` range.
|
||||
let use_nostr = match &nostr_diff {
|
||||
Ok(diff) => has_patch_link || !diff.files.is_empty(),
|
||||
Err(_) => true,
|
||||
};
|
||||
|
||||
let git = if use_nostr {
|
||||
None
|
||||
} else {
|
||||
let cache = cache.clone();
|
||||
let addr = addr.clone();
|
||||
let clone_urls = clone_urls.clone();
|
||||
let base = merge_base.clone();
|
||||
let tip = current_commit.clone();
|
||||
|
||||
Some(
|
||||
cx.background_spawn(async move {
|
||||
let repo = cache.ensure_clone(&addr, &clone_urls)?;
|
||||
|
||||
let workdir = repo
|
||||
.workdir()
|
||||
.ok_or_else(|| anyhow::anyhow!("repository has no worktree"))?
|
||||
.to_path_buf();
|
||||
|
||||
let tip =
|
||||
tip.ok_or_else(|| anyhow::anyhow!("pull request has no tip commit"))?;
|
||||
|
||||
let base = match base {
|
||||
Some(base) => base,
|
||||
// No `merge-base` tag. Use the merge base of the tip and the default branch.
|
||||
None => {
|
||||
let head = repo
|
||||
.head_id()
|
||||
.map_err(|_| anyhow::anyhow!("repository has no HEAD"))?;
|
||||
let tip_id = repo.rev_parse_single(tip.as_bytes())?;
|
||||
repo.merge_base(tip_id, head)?.to_string()
|
||||
}
|
||||
};
|
||||
|
||||
let diff = signed_git::worktree_commit_range_diff(&workdir, &base, &tip)?;
|
||||
let commits =
|
||||
signed_git::worktree_commit_range_commits(&workdir, &base, &tip)?;
|
||||
|
||||
Ok::<_, anyhow::Error>((diff, commits, workdir))
|
||||
let task: gpui::Task<Result<(), anyhow::Error>> =
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let nostr_diff = cx
|
||||
.background_spawn({
|
||||
let patch = patch.clone();
|
||||
async move { patch_diffs(&patch) }
|
||||
})
|
||||
.await,
|
||||
)
|
||||
};
|
||||
.await;
|
||||
|
||||
let (diff, commits, worktree) = match git {
|
||||
Some(Ok((diff, commits, worktree))) => (Ok(diff), commits, Some(worktree)),
|
||||
Some(Err(error)) => (Err(error), Vec::new(), None),
|
||||
None => (nostr_diff, nostr_commits, None),
|
||||
};
|
||||
let nostr_commits = cx
|
||||
.background_spawn({
|
||||
let patch = patch.clone();
|
||||
async move { patch_commits(&patch) }
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update_in(cx, |this, _window, cx| {
|
||||
this.loading = false;
|
||||
this.worktree = worktree;
|
||||
this.current_commit = current_commit.map(SharedString::from);
|
||||
this.commit_item_sizes = Rc::new(vec![size(px(0.), px(ROW_HEIGHT)); commits.len()]);
|
||||
this.commits = commits;
|
||||
// PRs without patch events, e.g. published by ngit, carry their changes in git.
|
||||
// Fetch the clone and diff the `merge-base..tip` range.
|
||||
let use_nostr = match &nostr_diff {
|
||||
Ok(diff) => has_patch_link || !diff.files.is_empty(),
|
||||
Err(_) => true,
|
||||
};
|
||||
|
||||
match diff {
|
||||
Ok(diff) => {
|
||||
this.pane.update(cx, |pane, cx| pane.set_diff(diff, cx));
|
||||
let git = if use_nostr {
|
||||
None
|
||||
} else {
|
||||
let cache = cache.clone();
|
||||
let addr = addr.clone();
|
||||
let clone_urls = clone_urls.clone();
|
||||
let base = merge_base.clone();
|
||||
let tip = current_commit.clone();
|
||||
|
||||
Some(
|
||||
cx.background_spawn(async move {
|
||||
let repo = cache.ensure_clone(&addr, &clone_urls)?;
|
||||
|
||||
let workdir = repo
|
||||
.workdir()
|
||||
.ok_or_else(|| anyhow::anyhow!("repository has no worktree"))?
|
||||
.to_path_buf();
|
||||
|
||||
let tip = tip
|
||||
.ok_or_else(|| anyhow::anyhow!("pull request has no tip commit"))?;
|
||||
|
||||
let base = match base {
|
||||
Some(base) => base,
|
||||
// No `merge-base` tag. Use the merge base of the tip and the default branch.
|
||||
None => {
|
||||
let head = repo
|
||||
.head_id()
|
||||
.map_err(|_| anyhow::anyhow!("repository has no HEAD"))?;
|
||||
let tip_id = repo.rev_parse_single(tip.as_bytes())?;
|
||||
repo.merge_base(tip_id, head)?.to_string()
|
||||
}
|
||||
};
|
||||
|
||||
let diff =
|
||||
signed_git::worktree_commit_range_diff(&workdir, &base, &tip)?;
|
||||
let commits =
|
||||
signed_git::worktree_commit_range_commits(&workdir, &base, &tip)?;
|
||||
|
||||
Ok::<_, anyhow::Error>((diff, commits, workdir))
|
||||
})
|
||||
.await,
|
||||
)
|
||||
};
|
||||
|
||||
let (diff, commits, worktree) = match git {
|
||||
Some(Ok((diff, commits, worktree))) => (Ok(diff), commits, Some(worktree)),
|
||||
Some(Err(error)) => (Err(error), Vec::new(), None),
|
||||
None => (nostr_diff, nostr_commits, None),
|
||||
};
|
||||
|
||||
this.update_in(cx, |this, _window, cx| {
|
||||
this.loading = false;
|
||||
this.worktree = worktree;
|
||||
this.current_commit = current_commit.map(SharedString::from);
|
||||
this.commit_item_sizes =
|
||||
Rc::new(vec![size(px(0.), px(ROW_HEIGHT)); commits.len()]);
|
||||
this.commits = commits;
|
||||
|
||||
match diff {
|
||||
Ok(diff) => {
|
||||
this.pane.update(cx, |pane, cx| pane.set_diff(diff, cx));
|
||||
}
|
||||
Err(error) => {
|
||||
this.error = Some(error.to_string().into());
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
this.error = Some(error.to_string().into());
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})?;
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
Ok(())
|
||||
});
|
||||
|
||||
self.tasks.retain(|task| !task.is_ready());
|
||||
self.tasks.push(task);
|
||||
task.detach();
|
||||
}
|
||||
|
||||
/// Open the diff of `commit_id` in the bottom dock of the area.
|
||||
@@ -706,66 +704,6 @@ fn open_update_pull_request_dialog(
|
||||
}
|
||||
|
||||
/// The `c` tag of a PR event, the commit the proposal points at.
|
||||
fn current_commit_of(root: &Event) -> Option<String> {
|
||||
root.tags
|
||||
.iter()
|
||||
.find_map(|tag| match Nip34Tag::parse(tag.as_slice()) {
|
||||
Ok(Nip34Tag::CurrentCommit(commit)) => Some(commit.to_string()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The `merge-base` tag of a PR event, as hex.
|
||||
///
|
||||
/// The most recent common ancestor with the target branch.
|
||||
fn merge_base_of(event: &Event) -> Option<String> {
|
||||
event
|
||||
.tags
|
||||
.iter()
|
||||
.find_map(|tag| match Nip34Tag::parse(tag.as_slice()) {
|
||||
Ok(Nip34Tag::MergeBase(commit)) => Some(commit.to_string()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The `clone` tag of a PR event.
|
||||
///
|
||||
/// URLs where the proposed branch can be fetched, or `None` if the PR has none.
|
||||
fn clone_urls_of(event: &Event) -> Option<Vec<String>> {
|
||||
event
|
||||
.tags
|
||||
.iter()
|
||||
.find_map(|tag| match Nip34Tag::parse(tag.as_slice()) {
|
||||
Ok(Nip34Tag::Clone(urls)) => Some(urls.iter().map(ToString::to_string).collect()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The `branch-name` tag of a PR event, if any.
|
||||
fn branch_name_of(event: &Event) -> Option<String> {
|
||||
event
|
||||
.tags
|
||||
.iter()
|
||||
.find_map(|tag| match Nip34Tag::parse(tag.as_slice()) {
|
||||
Ok(Nip34Tag::BranchName(name)) => Some(name),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The latest PR update, kind 1619, revising `root`.
|
||||
fn latest_update<'a>(events: impl Iterator<Item = &'a Event>, root: &Event) -> Option<&'a Event> {
|
||||
let root_hex = root.id.to_hex();
|
||||
events
|
||||
.filter(|e| e.kind == Kind::GitPullRequestUpdate)
|
||||
.filter(|e| e.pubkey == root.pubkey)
|
||||
.filter(|e| {
|
||||
e.tags
|
||||
.iter()
|
||||
.any(|t| t.kind() == "E" && t.content() == Some(root_hex.as_str()))
|
||||
})
|
||||
.max_by_key(|e| e.created_at)
|
||||
}
|
||||
|
||||
/// One-line commit metadata for the commits list.
|
||||
///
|
||||
/// Author and relative time, whichever is available.
|
||||
@@ -826,104 +764,9 @@ impl Render for PullRequestDetailView {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use nostr::prelude::{Tag, *};
|
||||
|
||||
use super::*;
|
||||
|
||||
const COMMIT_HEX: &str = "1111111111111111111111111111111111111111";
|
||||
const OTHER_ROOT_HEX: &str = "2222222222222222222222222222222222222222";
|
||||
|
||||
fn keys() -> Keys {
|
||||
Keys::new(
|
||||
SecretKey::from_hex("0000000000000000000000000000000000000000000000000000000000000001")
|
||||
.expect("valid secret key"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Build a signed event with a controlled `created_at`.
|
||||
fn signed(kind: Kind, tags: Vec<Tag>, created_at: u64) -> Event {
|
||||
EventBuilder::new(kind, "")
|
||||
.tags(tags)
|
||||
.custom_created_at(Timestamp::from(created_at))
|
||||
.finalize(&keys())
|
||||
.expect("signed event")
|
||||
}
|
||||
|
||||
fn pr_root() -> Event {
|
||||
signed(
|
||||
Kind::GitPullRequest,
|
||||
vec![
|
||||
Tag::parse(["c", COMMIT_HEX]).expect("valid tag"),
|
||||
Tag::parse(["branch-name", "feature/x"]).expect("valid tag"),
|
||||
],
|
||||
100,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_current_commit_and_branch_name() {
|
||||
let pr = pr_root();
|
||||
assert_eq!(current_commit_of(&pr).as_deref(), Some(COMMIT_HEX));
|
||||
assert_eq!(branch_name_of(&pr).as_deref(), Some("feature/x"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_none_without_pr_tags() {
|
||||
let pr = signed(Kind::GitPullRequest, vec![], 100);
|
||||
assert_eq!(current_commit_of(&pr), None);
|
||||
assert_eq!(branch_name_of(&pr), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latest_update_picks_newest_revision_of_the_root() {
|
||||
let root = pr_root();
|
||||
let root_hex = root.id.to_hex();
|
||||
|
||||
let revision = |created_at: u64| {
|
||||
signed(
|
||||
Kind::GitPullRequestUpdate,
|
||||
vec![Tag::parse(["E", &root_hex]).expect("valid tag")],
|
||||
created_at,
|
||||
)
|
||||
};
|
||||
// An update revising a different PR must be ignored even though it is newer.
|
||||
let unrelated = signed(
|
||||
Kind::GitPullRequestUpdate,
|
||||
vec![Tag::parse(["E", OTHER_ROOT_HEX]).expect("valid tag")],
|
||||
999,
|
||||
);
|
||||
|
||||
let events = [unrelated, revision(200), root.clone(), revision(300)];
|
||||
let latest = latest_update(events.iter(), &root).expect("an update");
|
||||
|
||||
assert_eq!(latest.created_at.as_secs(), 300);
|
||||
assert_eq!(latest.kind, Kind::GitPullRequestUpdate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latest_update_ignores_other_authors() {
|
||||
let root = pr_root();
|
||||
let root_hex = root.id.to_hex();
|
||||
let other = Keys::new(
|
||||
SecretKey::from_hex("0000000000000000000000000000000000000000000000000000000000000002")
|
||||
.expect("valid secret key"),
|
||||
);
|
||||
let stranger = EventBuilder::new(Kind::GitPullRequestUpdate, "")
|
||||
.tags([Tag::parse(["E", &root_hex]).expect("valid tag")])
|
||||
.custom_created_at(Timestamp::from(999))
|
||||
.finalize(&other)
|
||||
.expect("signed event");
|
||||
|
||||
// The tip of a PR is only mutable by its author.
|
||||
// A newer update from anyone else must not win.
|
||||
assert!(latest_update([&stranger, &root].into_iter(), &root).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latest_update_ignores_roots_without_revisions() {
|
||||
let root = pr_root();
|
||||
assert!(latest_update([&root].into_iter(), &root).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commit_meta_combines_author_and_time() {
|
||||
|
||||
Reference in New Issue
Block a user