279 lines
11 KiB
Rust
279 lines
11 KiB
Rust
use std::collections::HashSet;
|
|
|
|
use anyhow::Error;
|
|
use gpui::prelude::*;
|
|
use gpui::{Context, Entity, SharedString, Window};
|
|
use gpui_component::combobox::ComboboxState;
|
|
use gpui_component::searchable_list::SearchableVec;
|
|
|
|
use super::{RefKind, RepoDetailView};
|
|
use crate::views::repo::helpers::{build_tree_items, sorted_worktree_paths, tree_items};
|
|
|
|
impl RepoDetailView {
|
|
pub(super) fn switch_ref(
|
|
&mut self,
|
|
kind: RefKind,
|
|
name: SharedString,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if self.switching_ref {
|
|
return;
|
|
}
|
|
let Some(worktree) = self.worktree.clone() else {
|
|
return;
|
|
};
|
|
|
|
// Branches and tags are mutually exclusive states of HEAD.
|
|
// Selecting one clears the other selector.
|
|
// Remember the previous selections to restore them if the checkout fails.
|
|
let previous_branch = self.branch_select.read(cx).selected_value();
|
|
let previous_tag = self.tag_select.read(cx).selected_value();
|
|
|
|
match kind {
|
|
RefKind::Branch => {
|
|
self.tag_select
|
|
.update(cx, |state, cx| state.clear_selection(cx));
|
|
}
|
|
RefKind::Tag => {
|
|
self.branch_select
|
|
.update(cx, |state, cx| state.clear_selection(cx));
|
|
}
|
|
}
|
|
self.switching_ref = true;
|
|
// In-flight loads of the previous branch are discarded when they complete.
|
|
self.ref_generation += 1;
|
|
cx.notify();
|
|
|
|
let checkout_name = name.clone();
|
|
let task: gpui::Task<Result<(), Error>> = cx.spawn_in(window, async move |this, cx| {
|
|
let result = cx
|
|
.background_spawn(async move {
|
|
match kind {
|
|
RefKind::Branch => {
|
|
signed_git::worktree_checkout_branch(&worktree, &checkout_name)
|
|
}
|
|
RefKind::Tag => {
|
|
signed_git::worktree_checkout_tag(&worktree, &checkout_name)
|
|
}
|
|
}
|
|
})
|
|
.await;
|
|
|
|
this.update_in(cx, |this, window, cx| {
|
|
match result {
|
|
Ok(()) => this.reload_worktree(cx),
|
|
Err(error) => {
|
|
this.error = Some(format!("Failed to check out {name}: {error}").into());
|
|
this.switching_ref = false;
|
|
this.restore_selection(&this.branch_select, &previous_branch, window, cx);
|
|
this.restore_selection(&this.tag_select, &previous_tag, window, cx);
|
|
}
|
|
}
|
|
cx.notify();
|
|
})?;
|
|
|
|
Ok(())
|
|
});
|
|
|
|
task.detach();
|
|
}
|
|
|
|
fn restore_selection(
|
|
&self,
|
|
select: &Entity<ComboboxState<SearchableVec<SharedString>>>,
|
|
previous: &Option<SharedString>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
select.update(cx, |state, cx| match previous {
|
|
Some(value) => state.set_selected_values(std::slice::from_ref(value), window, cx),
|
|
None => state.clear_selection(cx),
|
|
});
|
|
}
|
|
|
|
fn reload_worktree(&mut self, cx: &mut Context<Self>) {
|
|
let Some(worktree) = self.worktree.clone() else {
|
|
return;
|
|
};
|
|
|
|
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)?;
|
|
// Build the tree off the main thread, like [`Self::load_repo`].
|
|
let tree = build_tree_items(&snapshot.entries);
|
|
let paths = sorted_worktree_paths(&snapshot.entries);
|
|
Ok::<_, Error>((snapshot, tree, paths))
|
|
})
|
|
.await;
|
|
|
|
this.update(cx, |this, cx| {
|
|
this.switching_ref = false;
|
|
match result {
|
|
Ok((snapshot, tree, paths)) => {
|
|
this.head_commit = snapshot.head_commit;
|
|
this.worktree_paths = paths;
|
|
// Rebuild the tree from scratch.
|
|
// Entries of the previous branch are gone.
|
|
// The expansion state goes with them.
|
|
this.tree_state.update(cx, |state, cx| {
|
|
state.set_items(tree_items(tree, false), cx);
|
|
});
|
|
|
|
// Drop cached previews and commits of the old branch.
|
|
this.selected_file = None;
|
|
this.files.clear();
|
|
this.file_order.clear();
|
|
this.preview_bytes = 0;
|
|
this.loading_files.clear();
|
|
this.commits.clear();
|
|
this.pending_commits.clear();
|
|
this.loading_commits = false;
|
|
this.md = None;
|
|
this.code = None;
|
|
this.readme_name = None;
|
|
this.all_commits = None;
|
|
this.loading_all_commits = false;
|
|
|
|
if let Some((path, bytes)) = snapshot.readme_path.zip(snapshot.readme) {
|
|
this.readme_name = Some(path.to_string_lossy().into());
|
|
this.load_commit(&path.to_string_lossy(), cx);
|
|
if let Ok(text) = String::from_utf8(bytes) {
|
|
this.set_markdown(None, &text, cx);
|
|
}
|
|
}
|
|
this.load_all_commits(cx);
|
|
}
|
|
Err(error) => {
|
|
this.error = Some(error.to_string().into());
|
|
this.head_commit = None;
|
|
this.worktree_paths.clear();
|
|
// The tree may show files that no longer exist.
|
|
this.tree_state.update(cx, |state, cx| {
|
|
state.set_items(Vec::new(), cx);
|
|
});
|
|
}
|
|
}
|
|
cx.notify();
|
|
})?;
|
|
|
|
Ok(())
|
|
});
|
|
|
|
task.detach();
|
|
}
|
|
|
|
/// Refresh the file explorer, previews and commit list after the mirror
|
|
/// caught up with the remote.
|
|
///
|
|
/// The checked-out branch fast-forwarded in place, so unlike
|
|
/// [`Self::reload_worktree`] this keeps the panel's selection and previews:
|
|
/// it rebuilds the tree, drops previews of files the refresh removed and
|
|
/// re-renders the README when it is on screen.
|
|
pub(super) fn catch_up_worktree(&mut self, cx: &mut Context<Self>) {
|
|
let Some(worktree) = self.worktree.clone() else {
|
|
return;
|
|
};
|
|
|
|
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)?;
|
|
let tree = build_tree_items(&snapshot.entries);
|
|
let paths = sorted_worktree_paths(&snapshot.entries);
|
|
Ok::<_, Error>((snapshot, tree, paths))
|
|
})
|
|
.await;
|
|
|
|
this.update(cx, |this, cx| {
|
|
match result {
|
|
Ok((snapshot, tree, paths)) => {
|
|
let head_changed = snapshot.head_commit.as_ref().map(|c| &c.id)
|
|
!= this.head_commit.as_ref().map(|c| &c.id);
|
|
|
|
// A fast-forward of a branch other than the checked-out
|
|
// one leaves the worktree untouched. Rebuilding the tree
|
|
// and re-parsing the README would flash the panel for
|
|
// nothing, so it is a no-op.
|
|
if !head_changed && paths == this.worktree_paths {
|
|
log::debug!("repo detail catch_up_worktree: no-op");
|
|
return;
|
|
}
|
|
|
|
log::debug!(
|
|
"repo detail catch_up_worktree: head_changed={head_changed} entries={}",
|
|
paths.len()
|
|
);
|
|
|
|
this.head_commit = snapshot.head_commit;
|
|
this.worktree_paths = paths;
|
|
this.tree_state.update(cx, |state, cx| {
|
|
state.set_items(tree_items(tree, false), cx);
|
|
});
|
|
|
|
// Drop previews of files the refresh removed from the worktree,
|
|
// everything else stays put.
|
|
let present: HashSet<String> = snapshot
|
|
.entries
|
|
.iter()
|
|
.map(|path| path.to_string_lossy().into_owned())
|
|
.collect();
|
|
|
|
let mut previewed: Vec<String> = Vec::new();
|
|
previewed.extend(this.files.keys().cloned());
|
|
previewed.extend(this.selected_file.clone().map(|p| p.to_string()));
|
|
|
|
if let Some(path) = this.md.as_ref().and_then(|md| md.path.clone()) {
|
|
previewed.push(path.to_string());
|
|
}
|
|
|
|
if let Some(path) = this.code.as_ref().map(|code| code.path.clone()) {
|
|
previewed.push(path.to_string());
|
|
}
|
|
|
|
previewed.sort();
|
|
previewed.dedup();
|
|
|
|
for path in previewed {
|
|
if !present.contains(&path) {
|
|
this.drop_preview_of(&path);
|
|
}
|
|
}
|
|
|
|
// Re-render the README when it is on screen, i.e. when no file preview is open.
|
|
if this.selected_file.is_none() {
|
|
match snapshot.readme_path.zip(snapshot.readme) {
|
|
Some((path, bytes)) => {
|
|
this.readme_name = Some(path.to_string_lossy().into());
|
|
if let Ok(text) = String::from_utf8(bytes) {
|
|
this.set_markdown(None, &text, cx);
|
|
}
|
|
}
|
|
None => {
|
|
this.md = None;
|
|
this.readme_name = None;
|
|
}
|
|
}
|
|
}
|
|
|
|
if head_changed {
|
|
this.all_commits = None;
|
|
this.loading_all_commits = false;
|
|
this.load_all_commits(cx);
|
|
}
|
|
}
|
|
Err(error) => {
|
|
this.error = Some(error.to_string().into());
|
|
}
|
|
}
|
|
cx.notify();
|
|
})?;
|
|
|
|
Ok(())
|
|
});
|
|
|
|
task.detach();
|
|
}
|
|
}
|