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, ) { 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> = 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>>, previous: &Option, window: &mut Window, cx: &mut Context, ) { 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) { let Some(worktree) = self.worktree.clone() else { return; }; let task: gpui::Task> = 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) { let Some(worktree) = self.worktree.clone() else { return; }; let task: gpui::Task> = 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 = snapshot .entries .iter() .map(|path| path.to_string_lossy().into_owned()) .collect(); let mut previewed: Vec = 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(); } }