chore: refactor the backend #17

Merged
reya merged 10 commits from chore/refactor into master 2026-09-10 09:43:36 +00:00
5 changed files with 1684 additions and 389 deletions
Showing only changes of commit 5f37f2ff2a - Show all commits
+32 -34
View File
@@ -325,8 +325,6 @@ pub struct CommitDiffView {
error: Option<SharedString>, error: Option<SharedString>,
/// Changed-files explorer and per-file diff, also used by the new PR panel's compare view. /// Changed-files explorer and per-file diff, also used by the new PR panel's compare view.
pane: Entity<DiffPane>, pane: Entity<DiffPane>,
/// In-flight tasks, pruned on every push.
tasks: Vec<gpui::Task<Result<(), anyhow::Error>>>,
} }
impl CommitDiffView { impl CommitDiffView {
@@ -358,7 +356,6 @@ impl CommitDiffView {
loading: true, loading: true,
error: None, error: None,
pane, pane,
tasks: Vec::new(),
} }
} }
@@ -371,42 +368,43 @@ impl CommitDiffView {
let worktree = self.worktree.clone(); let worktree = self.worktree.clone();
let id = self.commit.id.clone(); let id = self.commit.id.clone();
let task = cx.spawn_in(window, async move |this, cx| { let task: gpui::Task<Result<(), anyhow::Error>> =
let commit = cx cx.spawn_in(window, async move |this, cx| {
.background_spawn({ let commit = cx
let worktree = worktree.clone(); .background_spawn({
let id = id.clone(); let worktree = worktree.clone();
async move { signed_git::worktree_commit(&worktree, &id) } let id = id.clone();
}) async move { signed_git::worktree_commit(&worktree, &id) }
.await; })
let diff = cx .await;
.background_spawn({ let diff = cx
let worktree = worktree.clone(); .background_spawn({
let id = id.clone(); let worktree = worktree.clone();
async move { signed_git::worktree_commit_diff(&worktree, &id) } let id = id.clone();
}) async move { signed_git::worktree_commit_diff(&worktree, &id) }
.await; })
.await;
this.update_in(cx, |this, _window, cx| { this.update_in(cx, |this, _window, cx| {
this.loading = false; this.loading = false;
if let Ok(Some(commit)) = commit { if let Ok(Some(commit)) = commit {
this.commit = commit; this.commit = commit;
}
match diff {
Ok(diff) => {
this.pane.update(cx, |pane, cx| pane.set_diff(diff, cx));
} }
Err(error) => { match diff {
this.error = Some(error.to_string().into()); 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. /// Header with the commit id, summary, author/time and overall change stats.
+30 -32
View File
@@ -10,8 +10,8 @@ use gix::Repository;
use gpui::prelude::*; use gpui::prelude::*;
use gpui::{ use gpui::{
Action, Anchor, AnyElement, App, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Action, Anchor, AnyElement, App, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
Focusable, PathPromptOptions, Pixels, Render, SharedString, Size, Subscription, Task, Focusable, PathPromptOptions, Pixels, Render, SharedString, Size, Subscription, WeakEntity,
WeakEntity, Window, div, px, relative, size, transparent_white, Window, div, px, relative, size, transparent_white,
}; };
use gpui_base::{Button as BaseButton, Disableable, Popover}; use gpui_base::{Button as BaseButton, Disableable, Popover};
use gpui_component::alert::Alert; use gpui_component::alert::Alert;
@@ -175,9 +175,6 @@ pub struct RepoDetailView {
/// Bumped on every branch/tag switch. /// Bumped on every branch/tag switch.
/// In-flight loads with an older generation are discarded when they complete. /// In-flight loads with an older generation are discarded when they complete.
ref_generation: u64, 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 keeping the selectors' confirm events alive.
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
/// `(path, branch)` ready-suggestions dismissed by the user, per panel. /// `(path, branch)` ready-suggestions dismissed by the user, per panel.
@@ -346,7 +343,6 @@ impl RepoDetailView {
push_statuses: Vec::new(), push_statuses: Vec::new(),
pending_upstream: None, pending_upstream: None,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
tasks: Vec::new(),
_subscriptions: subscriptions, _subscriptions: subscriptions,
} }
} }
@@ -363,7 +359,7 @@ impl RepoDetailView {
// Local repositories live on disk at their scan path. // Local repositories live on disk at their scan path.
// No clone step or network refresh applies here. // No clone step or network refresh applies here.
if let Some(local_path) = self.local_path.clone() { 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 let data = cx
.background_spawn(async move { .background_spawn(async move {
let repo = gix::open(&local_path)?; let repo = gix::open(&local_path)?;
@@ -383,7 +379,7 @@ impl RepoDetailView {
Ok(()) Ok(())
}); });
self.tasks.push(task); task.detach();
return; return;
} }
@@ -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 disk = disk.await;
let had_clone = matches!(&disk, Ok(Some(_))); let had_clone = matches!(&disk, Ok(Some(_)));
@@ -531,7 +527,7 @@ impl RepoDetailView {
Ok(()) Ok(())
}); });
self.tasks.push(task); task.detach();
} }
/// Apply the loaded repository data. /// Apply the loaded repository data.
@@ -619,7 +615,7 @@ impl RepoDetailView {
prompt: Some("Clone".into()), 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. // `Ok(Ok(Some(paths)))` means the user picked a folder.
// A cancel or picker failure resolves to anything else. // A cancel or picker failure resolves to anything else.
let picked = match prompt.await { let picked = match prompt.await {
@@ -649,7 +645,7 @@ impl RepoDetailView {
Ok(()) Ok(())
}); });
self.tasks.push(task); task.detach();
} }
/// Preview the file at `path`, relative to the worktree root. /// Preview the file at `path`, relative to the worktree root.
@@ -703,7 +699,7 @@ impl RepoDetailView {
self.load_commit(&path, cx); self.load_commit(&path, cx);
let generation = self.ref_generation; 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 path_for_read = path.clone();
let content = cx let content = cx
.background_spawn(async move { .background_spawn(async move {
@@ -772,7 +768,7 @@ impl RepoDetailView {
Ok(()) Ok(())
}); });
self.tasks.push(task); task.detach();
} }
/// Queue `path` for the per-file commit query. /// Queue `path` for the per-file commit query.
@@ -804,7 +800,7 @@ impl RepoDetailView {
let paths = std::mem::take(&mut self.pending_commits); let paths = std::mem::take(&mut self.pending_commits);
let generation = self.ref_generation; 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 rels: Vec<PathBuf> = paths.iter().map(PathBuf::from).collect();
let result = cx let result = cx
.background_spawn( .background_spawn(
@@ -834,7 +830,7 @@ impl RepoDetailView {
Ok(()) Ok(())
}); });
self.tasks.push(task); task.detach();
} }
/// Walk all commits reachable from HEAD on a background task. /// Walk all commits reachable from HEAD on a background task.
@@ -852,7 +848,7 @@ impl RepoDetailView {
self.loading_all_commits = true; self.loading_all_commits = true;
let generation = self.ref_generation; 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 let result = cx
.background_spawn(async move { signed_git::worktree_all_commits(&worktree) }) .background_spawn(async move { signed_git::worktree_all_commits(&worktree) })
.await; .await;
@@ -876,7 +872,7 @@ impl RepoDetailView {
Ok(()) Ok(())
}); });
self.tasks.push(task); task.detach();
} }
/// Open a new panel showing the diff of `commit_id`. /// Open a new panel showing the diff of `commit_id`.
@@ -909,8 +905,9 @@ impl RepoDetailView {
self.error = None; self.error = None;
cx.notify(); cx.notify();
self.tasks store
.push(store.update(cx, |store, cx| store.push_repository(cx))); .update(cx, |store, cx| store.push_repository(cx))
.detach();
} }
/// Push the unpushed commits of the local checkout at `path`. /// Push the unpushed commits of the local checkout at `path`.
@@ -931,7 +928,7 @@ impl RepoDetailView {
self.error = None; self.error = None;
cx.notify(); 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. // The store owns the push, its busy flag and error reporting.
let push = this.update_in(cx, |_this, _window, cx| { let push = this.update_in(cx, |_this, _window, cx| {
store.update(cx, |store, cx| store.push_checkout(path.clone(), cx)) store.update(cx, |store, cx| store.push_checkout(path.clone(), cx))
@@ -948,7 +945,7 @@ impl RepoDetailView {
Ok(()) Ok(())
}); });
self.tasks.push(task); task.detach();
} }
/// Delete the repository from nostr, announcement, state and activity. /// Delete the repository from nostr, announcement, state and activity.
@@ -956,8 +953,9 @@ impl RepoDetailView {
let Some(store) = self.store.clone() else { let Some(store) = self.store.clone() else {
return; return;
}; };
self.tasks store
.push(store.update(cx, |store, cx| store.delete_repository(cx))); .update(cx, |store, cx| store.delete_repository(cx))
.detach();
} }
/// Open the issues list panel in the dock area. /// Open the issues list panel in the dock area.
@@ -1025,7 +1023,7 @@ impl RepoDetailView {
}); });
self.pending_upstream = Some(addr); 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 { for _ in 0..60 {
cx.background_executor() cx.background_executor()
.timer(Duration::from_millis(250)) .timer(Duration::from_millis(250))
@@ -1060,7 +1058,7 @@ impl RepoDetailView {
Ok(()) Ok(())
}); });
self.tasks.push(task); task.detach();
} }
/// Check out `name`, a branch or tag picked in the header. /// Check out `name`, a branch or tag picked in the header.
@@ -1101,7 +1099,7 @@ impl RepoDetailView {
cx.notify(); cx.notify();
let checkout_name = name.clone(); 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 let result = cx
.background_spawn(async move { .background_spawn(async move {
match kind { match kind {
@@ -1131,7 +1129,7 @@ impl RepoDetailView {
Ok(()) Ok(())
}); });
self.tasks.push(task); task.detach();
} }
/// Restore a selector to `previous`, or clear it after a failed switch. /// Restore a selector to `previous`, or clear it after a failed switch.
@@ -1157,7 +1155,7 @@ impl RepoDetailView {
return; return;
}; };
let task = cx.spawn(async move |this, cx| { let task: gpui::Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
let result = cx let result = cx
.background_spawn(async move { .background_spawn(async move {
let snapshot = signed_git::worktree_snapshot(&worktree)?; let snapshot = signed_git::worktree_snapshot(&worktree)?;
@@ -1218,7 +1216,7 @@ impl RepoDetailView {
Ok(()) Ok(())
}); });
self.tasks.push(task); task.detach();
} }
/// Refresh the file explorer, previews and commit list after the mirror /// Refresh the file explorer, previews and commit list after the mirror
@@ -1233,7 +1231,7 @@ impl RepoDetailView {
return; return;
}; };
let task = cx.spawn(async move |this, cx| { let task: gpui::Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
let result = cx let result = cx
.background_spawn(async move { .background_spawn(async move {
let snapshot = signed_git::worktree_snapshot(&worktree)?; let snapshot = signed_git::worktree_snapshot(&worktree)?;
@@ -1314,7 +1312,7 @@ impl RepoDetailView {
Ok(()) Ok(())
}); });
self.tasks.push(task); task.detach();
} }
/// Drop the cached preview, editor and commit state of `path`. /// 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::prelude::*;
use gpui::{ use gpui::{
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, PathPromptOptions, AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, PathPromptOptions,
Pixels, Render, SharedString, Size, Subscription, Task, WeakEntity, Window, div, px, relative, Pixels, Render, SharedString, Size, Subscription, WeakEntity, Window, div, px, relative, size,
size,
}; };
use gpui_base::{Button as BaseButton, StyledExt}; use gpui_base::{Button as BaseButton, StyledExt};
use gpui_component::button::{Button, ButtonVariants}; use gpui_component::button::{Button, ButtonVariants};
@@ -79,7 +78,6 @@ pub struct NewPullRequestView {
scroll_handle: VirtualListScrollHandle, scroll_handle: VirtualListScrollHandle,
item_sizes: Rc<Vec<Size<Pixels>>>, item_sizes: Rc<Vec<Size<Pixels>>>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
tasks: Vec<Task<Result<(), anyhow::Error>>>,
} }
/// A fork-backed compare. /// A fork-backed compare.
@@ -363,7 +361,6 @@ impl NewPullRequestView {
scroll_handle: VirtualListScrollHandle::new(), scroll_handle: VirtualListScrollHandle::new(),
item_sizes: Rc::new(Vec::new()), item_sizes: Rc::new(Vec::new()),
_subscriptions: subscriptions, _subscriptions: subscriptions,
tasks: Vec::new(),
}; };
// Prefill with the store's freshest associated checkout, no folder dialog. // Prefill with the store's freshest associated checkout, no folder dialog.
@@ -426,25 +423,26 @@ impl NewPullRequestView {
prompt: Some("Choose local checkout".into()), prompt: Some("Choose local checkout".into()),
}); });
let task = cx.spawn_in(window, async move |this, cx| { let task: gpui::Task<Result<(), anyhow::Error>> =
// `Ok(Ok(Some(paths)))` means the user picked a folder. cx.spawn_in(window, async move |this, cx| {
// A cancel or picker failure resolves to anything else. // `Ok(Ok(Some(paths)))` means the user picked a folder.
let picked = match prompt.await { // A cancel or picker failure resolves to anything else.
Ok(Ok(Some(mut paths))) => paths.pop(), let picked = match prompt.await {
_ => None, Ok(Ok(Some(mut paths))) => paths.pop(),
}; _ => None,
};
let Some(path) = picked else { let Some(path) = picked else {
return Ok(()); return Ok(());
}; };
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {
this.apply_folder_path(path, window, cx); this.apply_folder_path(path, window, cx);
})?; })?;
Ok(()) Ok(())
}); });
self.tasks.push(task); task.detach();
} }
/// Apply `path` as the local checkout, no picker. /// Apply `path` as the local checkout, no picker.
@@ -453,28 +451,29 @@ impl NewPullRequestView {
fn apply_folder_path(&mut self, path: PathBuf, window: &mut Window, cx: &mut Context<Self>) { fn apply_folder_path(&mut self, path: PathBuf, window: &mut Window, cx: &mut Context<Self>) {
let path = path.to_string_lossy().to_string(); let path = path.to_string_lossy().to_string();
let task = cx.spawn_in(window, async move |this, cx| { let task: gpui::Task<Result<(), anyhow::Error>> =
// Branches and the current branch are read off the UI thread. cx.spawn_in(window, async move |this, cx| {
let info = cx // Branches and the current branch are read off the UI thread.
.background_spawn({ let info = cx
let path = path.clone(); .background_spawn({
async move { let path = path.clone();
let repo = gix::open(Path::new(&path)).ok()?; async move {
let branches = let repo = gix::open(Path::new(&path)).ok()?;
signed_git::worktree_branches(Path::new(&path)).unwrap_or_default(); let branches =
let current = signed_git::current_branch(&repo).ok().flatten(); signed_git::worktree_branches(Path::new(&path)).unwrap_or_default();
Some((branches, current)) let current = signed_git::current_branch(&repo).ok().flatten();
} Some((branches, current))
}) }
.await; })
.await;
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {
this.apply_checkout(path, info, window, cx); this.apply_checkout(path, info, window, cx);
})?; })?;
Ok(()) Ok(())
}); });
self.tasks.push(task); task.detach();
} }
/// Apply a picked checkout, filling the selectors and loading the compare. /// Apply a picked checkout, filling the selectors and loading the compare.
@@ -615,88 +614,89 @@ impl NewPullRequestView {
self.error = None; self.error = None;
cx.notify(); cx.notify();
let task = cx.spawn_in(window, async move |this, cx| { let task: gpui::Task<Result<(), anyhow::Error>> =
// The fork and base must share history for a merge-base to exist. cx.spawn_in(window, async move |this, cx| {
// The target's mirror is the object store both sides land in. // The fork and base must share history for a merge-base to exist.
// `ensure_clone` fetches `origin` when the mirror already exists. // The target's mirror is the object store both sides land in.
let result = cx // `ensure_clone` fetches `origin` when the mirror already exists.
.background_spawn({ let result = cx
let cache = cache.clone(); .background_spawn({
let base = base.clone(); let cache = cache.clone();
let base_clone_urls = base_clone_urls.clone(); let base = base.clone();
let namespace = namespace.clone(); let base_clone_urls = base_clone_urls.clone();
let clone_urls = clone_urls.clone(); let namespace = namespace.clone();
let mirror_path = mirror_path.clone(); let clone_urls = clone_urls.clone();
async move { let mirror_path = mirror_path.clone();
// The fork and base must share history for a merge-base to exist. async move {
// The target's mirror is the object store both sides land in. // The fork and base must share history for a merge-base to exist.
// `ensure_clone` fetches `origin` when the mirror already exists. // The target's mirror is the object store both sides land in.
cache.ensure_clone(&base, &base_clone_urls)?; // `ensure_clone` fetches `origin` when the mirror already exists.
cache.ensure_clone(&base, &base_clone_urls)?;
// Prune stale imports of any fork. // Prune stale imports of any fork.
// Then import this fork's heads under its namespace. // Then import this fork's heads under its namespace.
delete_refs_with_prefix(&mirror_path, "refs/fork")?; delete_refs_with_prefix(&mirror_path, "refs/fork")?;
fetch_repo_refs( fetch_repo_refs(
&mirror_path, &mirror_path,
&clone_urls, &clone_urls,
&format!("+refs/heads/*:refs/fork/{namespace}/*"), &format!("+refs/heads/*:refs/fork/{namespace}/*"),
)?; )?;
// Both branch lists are short names, sorted like the checkout's. // Both branch lists are short names, sorted like the checkout's.
let strip = |refs: Vec<String>, prefix: &str| { let strip = |refs: Vec<String>, prefix: &str| {
let mut names: Vec<String> = refs let mut names: Vec<String> = refs
.into_iter() .into_iter()
.filter_map(|name| { .filter_map(|name| {
name.strip_prefix(prefix) name.strip_prefix(prefix)
.map(|rest| rest.trim_start_matches('/').to_owned()) .map(|rest| rest.trim_start_matches('/').to_owned())
}) })
.filter(|name| !name.is_empty()) .filter(|name| !name.is_empty())
.collect(); .collect();
names.sort(); names.sort();
names names
}; };
let base_branches = strip( let base_branches = strip(
refs_with_prefix(&mirror_path, "refs/remotes/origin")?, refs_with_prefix(&mirror_path, "refs/remotes/origin")?,
"refs/remotes/origin", "refs/remotes/origin",
); );
let compare_branches = strip( let compare_branches = strip(
refs_with_prefix(&mirror_path, &format!("refs/fork/{namespace}"))?, refs_with_prefix(&mirror_path, &format!("refs/fork/{namespace}"))?,
&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| { this.apply_fork(
// A source switch mid-flight discards the stale result. announcement,
// E.g. the user picked a folder while the fork was fetching. mirror_path,
let applied = this.fork.as_ref().map(|fork| fork.announcement.addr()); namespace,
if applied != expected_fork { result,
this.loading = false; keep_base,
cx.notify(); keep_compare,
return; window,
} cx,
);
})?;
this.apply_fork( Ok(())
announcement, });
mirror_path, task.detach();
namespace,
result,
keep_base,
keep_compare,
window,
cx,
);
})?;
Ok(())
});
self.tasks.push(task);
} }
/// Apply an imported fork, filling the selectors and loading the compare. /// Apply an imported fork, filling the selectors and loading the compare.
@@ -834,66 +834,68 @@ impl NewPullRequestView {
return; return;
} }
let task = cx.spawn_in(window, async move |this, cx| { let task: gpui::Task<Result<(), anyhow::Error>> =
let result = cx cx.spawn_in(window, async move |this, cx| {
.background_spawn({ let result = cx
let repo_path = repo_path.clone(); .background_spawn({
let base = base.clone(); let repo_path = repo_path.clone();
let compare = compare.clone(); let base = base.clone();
let base_name = base_name.clone(); let compare = compare.clone();
let compare_name = compare_name.clone(); let base_name = base_name.clone();
async move { let compare_name = compare_name.clone();
let merge_base = merge_base(Path::new(&repo_path), &base, &compare)? async move {
.ok_or_else(|| { let merge_base = merge_base(Path::new(&repo_path), &base, &compare)?
anyhow::anyhow!( .ok_or_else(|| {
"{base_name} and {compare_name} share no common ancestor" anyhow::anyhow!(
) "{base_name} and {compare_name} share no common ancestor"
})?; )
let commits = worktree_commit_range_commits( })?;
Path::new(&repo_path), let commits = worktree_commit_range_commits(
&merge_base, Path::new(&repo_path),
&compare, &merge_base,
)?; &compare,
let diff = worktree_commit_range_diff( )?;
Path::new(&repo_path), let diff = worktree_commit_range_diff(
&merge_base, Path::new(&repo_path),
&compare, &merge_base,
)?; &compare,
Ok::<_, anyhow::Error>((merge_base, commits, diff)) )?;
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;
} }
}) this.loading = false;
.await;
this.update_in(cx, |this, _window, cx| { match result {
// A stale result, branches changed mid-flight, must not clobber a newer compare. Ok((merge_base, commits, diff)) => {
if generation != this.compare_generation { this.merge_base = Some(merge_base);
return; let count = commits.len();
} this.item_sizes =
this.loading = false; Rc::new(vec![size(px(0.), px(COMMIT_ROW_HEIGHT)); count]);
this.commits = Some(commits);
match result { this.pane.update(cx, |pane, cx| pane.set_diff(diff, cx));
Ok((merge_base, commits, diff)) => { }
this.merge_base = Some(merge_base); Err(error) => {
let count = commits.len(); this.merge_base = None;
this.item_sizes = Rc::new(vec![size(px(0.), px(COMMIT_ROW_HEIGHT)); count]); this.commits = None;
this.commits = Some(commits); this.pane.update(cx, |pane, cx| pane.clear(cx));
this.pane.update(cx, |pane, cx| pane.set_diff(diff, 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. /// Publish the pull request.
@@ -927,76 +929,78 @@ impl NewPullRequestView {
self.error = None; self.error = None;
cx.notify(); cx.notify();
let task = cx.spawn_in(window, async move |this, cx| { let task: gpui::Task<Result<(), anyhow::Error>> =
// Regenerate the series at submit time. cx.spawn_in(window, async move |this, cx| {
// The published patch covers the current tip of the compare branch. // Regenerate the series at submit time.
let patch = cx // The published patch covers the current tip of the compare branch.
.background_spawn({ let patch = cx
let repo_path = repo_path.clone(); .background_spawn({
let merge_base = merge_base.clone(); let repo_path = repo_path.clone();
let compare_ref = compare_ref.clone(); let merge_base = merge_base.clone();
async move { let compare_ref = compare_ref.clone();
format_patch_between(Path::new(&repo_path), &merge_base, &compare_ref) 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(
(!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);
});
} }
})
.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(());
}
};
cx.notify(); this.update_in(cx, |this, window, cx| {
})?; this.submitting = false;
Ok(()) store.update(cx, |store, cx| {
}); store.open_pull_request(
(!subject.is_empty()).then_some(subject),
description,
Some(branch_name),
patch,
false,
Some(merge_base),
Some(repo_path),
cx,
);
});
self.tasks.push(task); // 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();
})?;
Ok(())
});
task.detach();
} }
/// Open the diff of `commit_id`, from the Commits tab, in a new panel. /// Open the diff of `commit_id`, from the Commits tab, in a new panel.
@@ -6,7 +6,7 @@ use dock::{BasePanel, DockArea, DockPlacement, Panel, PanelEvent, panel_handle};
use gpui::prelude::*; use gpui::prelude::*;
use gpui::{ use gpui::{
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render, 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::button::{Button, ButtonVariants};
use gpui_component::clipboard::Clipboard; use gpui_component::clipboard::Clipboard;
@@ -66,8 +66,6 @@ pub struct PullRequestDetailView {
commit_item_sizes: Rc<Vec<Size<Pixels>>>, commit_item_sizes: Rc<Vec<Size<Pixels>>>,
/// Virtual list state of the commits tab. /// Virtual list state of the commits tab.
commit_scroll_handle: VirtualListScrollHandle, commit_scroll_handle: VirtualListScrollHandle,
/// In-flight tasks, finished tasks are pruned on every push.
tasks: Vec<Task<Result<(), anyhow::Error>>>,
} }
impl PullRequestDetailView { impl PullRequestDetailView {
@@ -106,7 +104,6 @@ impl PullRequestDetailView {
pane, pane,
commit_item_sizes: Rc::new(Vec::new()), commit_item_sizes: Rc::new(Vec::new()),
commit_scroll_handle: VirtualListScrollHandle::new(), commit_scroll_handle: VirtualListScrollHandle::new(),
tasks: Vec::new(),
} }
} }
@@ -162,101 +159,103 @@ impl PullRequestDetailView {
self.description = description.into(); self.description = description.into();
let task = cx.spawn_in(window, async move |this, cx| { let task: gpui::Task<Result<(), anyhow::Error>> =
let nostr_diff = cx cx.spawn_in(window, async move |this, cx| {
.background_spawn({ let nostr_diff = cx
let patch = patch.clone(); .background_spawn({
async move { patch_diffs(&patch) } 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))
}) })
.await, .await;
)
};
let (diff, commits, worktree) = match git { let nostr_commits = cx
Some(Ok((diff, commits, worktree))) => (Ok(diff), commits, Some(worktree)), .background_spawn({
Some(Err(error)) => (Err(error), Vec::new(), None), let patch = patch.clone();
None => (nostr_diff, nostr_commits, None), async move { patch_commits(&patch) }
}; })
.await;
this.update_in(cx, |this, _window, cx| { // PRs without patch events, e.g. published by ngit, carry their changes in git.
this.loading = false; // Fetch the clone and diff the `merge-base..tip` range.
this.worktree = worktree; let use_nostr = match &nostr_diff {
this.current_commit = current_commit.map(SharedString::from); Ok(diff) => has_patch_link || !diff.files.is_empty(),
this.commit_item_sizes = Rc::new(vec![size(px(0.), px(ROW_HEIGHT)); commits.len()]); Err(_) => true,
this.commits = commits; };
match diff { let git = if use_nostr {
Ok(diff) => { None
this.pane.update(cx, |pane, cx| pane.set_diff(diff, cx)); } 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()); task.detach();
self.tasks.push(task);
} }
/// Open the diff of `commit_id` in the bottom dock of the area. /// Open the diff of `commit_id` in the bottom dock of the area.
File diff suppressed because it is too large Load Diff