Files
signed/crates/workspace/src/views/repo/refs.rs
T
2026-09-13 13:36:27 +07:00

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();
}
}