Cargo.lock 🔗
@@ -5370,6 +5370,7 @@ dependencies = [
"serde_derive",
"serde_json",
"settings",
+ "strum",
"theme",
"time",
"ui",
Conrad Irwin created
Adds the non-entry specific right click menu to the panel, and the
features contained therin:
* Stage all
* Discard Tracked Changes
* Trash Untracked Files
Also changes the naming from "Changes"/"New" to better match Git's
terminology (though not convinced on this, it was awkward to describe
"Discard Changes" without a way to distinguish between the changes and
the files containing them).
Release Notes:
- N/A
Cargo.lock | 1
crates/collab/src/rpc.rs | 1
crates/git/src/git.rs | 3
crates/git/src/repository.rs | 30 ++
crates/git_ui/Cargo.toml | 1
crates/git_ui/src/git_panel.rs | 426 +++++++++++++++++++++++++++++------
crates/project/src/git.rs | 82 ++++++
crates/proto/proto/zed.proto | 11
crates/proto/src/proto.rs | 3
9 files changed, 482 insertions(+), 76 deletions(-)
@@ -5370,6 +5370,7 @@ dependencies = [
"serde_derive",
"serde_json",
"settings",
+ "strum",
"theme",
"time",
"ui",
@@ -397,6 +397,7 @@ impl Server {
.add_request_handler(forward_mutating_project_request::<proto::Commit>)
.add_request_handler(forward_read_only_project_request::<proto::GitShow>)
.add_request_handler(forward_read_only_project_request::<proto::GitReset>)
+ .add_request_handler(forward_read_only_project_request::<proto::GitCheckoutFiles>)
.add_request_handler(forward_mutating_project_request::<proto::SetIndexText>)
.add_request_handler(forward_mutating_project_request::<proto::OpenCommitMessageBuffer>)
.add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
@@ -37,7 +37,8 @@ actions!(
// editor::RevertSelectedHunks
StageAll,
UnstageAll,
- RevertAll,
+ DiscardTrackedChanges,
+ TrashUntrackedFiles,
Uncommit,
Commit,
ClearCommitMessage
@@ -111,6 +111,7 @@ pub trait GitRepository: Send + Sync {
fn branch_exits(&self, _: &str) -> Result<bool>;
fn reset(&self, commit: &str, mode: ResetMode) -> Result<()>;
+ fn checkout_files(&self, commit: &str, paths: &[RepoPath]) -> Result<()>;
fn show(&self, commit: &str) -> Result<CommitDetails>;
@@ -233,6 +234,31 @@ impl GitRepository for RealGitRepository {
Ok(())
}
+ fn checkout_files(&self, commit: &str, paths: &[RepoPath]) -> Result<()> {
+ if paths.is_empty() {
+ return Ok(());
+ }
+ let working_directory = self
+ .repository
+ .lock()
+ .workdir()
+ .context("failed to read git work directory")?
+ .to_path_buf();
+
+ let output = new_std_command(&self.git_binary_path)
+ .current_dir(&working_directory)
+ .args(["checkout", commit, "--"])
+ .args(paths.iter().map(|path| path.as_ref()))
+ .output()?;
+ if !output.status.success() {
+ return Err(anyhow!(
+ "Failed to checkout files:\n{}",
+ String::from_utf8_lossy(&output.stderr)
+ ));
+ }
+ Ok(())
+ }
+
fn load_index_text(&self, path: &RepoPath) -> Option<String> {
fn logic(repo: &git2::Repository, path: &RepoPath) -> Result<Option<String>> {
const STAGE_NORMAL: i32 = 0;
@@ -617,6 +643,10 @@ impl GitRepository for FakeGitRepository {
unimplemented!()
}
+ fn checkout_files(&self, _: &str, _: &[RepoPath]) -> Result<()> {
+ unimplemented!()
+ }
+
fn path(&self) -> PathBuf {
let state = self.state.lock();
state.path.clone()
@@ -36,6 +36,7 @@ serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
settings.workspace = true
+strum.workspace = true
theme.workspace = true
time.workspace = true
ui.workspace = true
@@ -1,9 +1,9 @@
use crate::git_panel_settings::StatusStyle;
use crate::repository_selector::RepositorySelectorPopoverMenu;
-use crate::ProjectDiff;
use crate::{
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
};
+use crate::{project_diff, ProjectDiff};
use collections::HashMap;
use db::kvp::KEY_VALUE_STORE;
use editor::commit_tooltip::CommitTooltip;
@@ -13,6 +13,7 @@ use editor::{
};
use git::repository::{CommitDetails, ResetMode};
use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged};
+use git::{DiscardTrackedChanges, StageAll, TrashUntrackedFiles, UnstageAll};
use gpui::*;
use itertools::Itertools;
use language::{markdown, Buffer, File, ParsedMarkdown};
@@ -26,13 +27,13 @@ use project::{
use serde::{Deserialize, Serialize};
use settings::Settings as _;
use std::{collections::HashSet, path::PathBuf, sync::Arc, time::Duration, usize};
+use strum::{IntoEnumIterator, VariantNames};
use time::OffsetDateTime;
use ui::{
prelude::*, ButtonLike, Checkbox, ContextMenu, Divider, DividerColor, ElevationIndex, ListItem,
ListItemSpacing, Scrollbar, ScrollbarState, Tooltip,
};
use util::{maybe, ResultExt, TryFutureExt};
-use workspace::SaveIntent;
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
notifications::{DetachAndPromptErr, NotificationId},
@@ -51,6 +52,21 @@ actions!(
]
);
+fn prompt<T>(msg: &str, detail: Option<&str>, window: &mut Window, cx: &mut App) -> Task<Result<T>>
+where
+ T: IntoEnumIterator + VariantNames + 'static,
+{
+ let rx = window.prompt(PromptLevel::Info, msg, detail, &T::VARIANTS, cx);
+ cx.spawn(|_| async move { Ok(T::iter().nth(rx.await?).unwrap()) })
+}
+
+#[derive(strum::EnumIter, strum::VariantNames)]
+#[strum(serialize_all = "title_case")]
+enum TrashCancel {
+ Trash,
+ Cancel,
+}
+
const GIT_PANEL_KEY: &str = "GitPanel";
const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
@@ -112,8 +128,8 @@ impl GitHeaderEntry {
pub fn title(&self) -> &'static str {
match self.header {
Section::Conflict => "Conflicts",
- Section::Tracked => "Changes",
- Section::New => "New",
+ Section::Tracked => "Tracked",
+ Section::New => "Untracked",
}
}
}
@@ -142,9 +158,17 @@ pub struct GitStatusEntry {
pub(crate) is_staged: Option<bool>,
}
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+enum TargetStatus {
+ Staged,
+ Unstaged,
+ Reverted,
+ Unchanged,
+}
+
struct PendingOperation {
finished: bool,
- will_become_staged: bool,
+ target_status: TargetStatus,
repo_paths: HashSet<RepoPath>,
op_id: usize,
}
@@ -599,7 +623,7 @@ impl GitPanel {
});
}
- fn revert(
+ fn revert_selected(
&mut self,
_: &editor::actions::RevertFile,
window: &mut Window,
@@ -608,28 +632,37 @@ impl GitPanel {
maybe!({
let list_entry = self.entries.get(self.selected_entry?)?.clone();
let entry = list_entry.status_entry()?;
- let active_repo = self.active_repository.as_ref()?;
+ self.revert_entry(&entry, window, cx);
+ Some(())
+ });
+ }
+
+ fn revert_entry(
+ &mut self,
+ entry: &GitStatusEntry,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ maybe!({
+ let active_repo = self.active_repository.clone()?;
let path = active_repo
.read(cx)
.repo_path_to_project_path(&entry.repo_path)?;
let workspace = self.workspace.clone();
if entry.status.is_staged() != Some(false) {
- self.update_staging_area_for_entries(false, vec![entry.repo_path.clone()], cx);
+ self.perform_stage(false, vec![entry.repo_path.clone()], cx);
}
+ let filename = path.path.file_name()?.to_string_lossy();
- if entry.status.is_created() {
- let prompt = window.prompt(
- PromptLevel::Info,
- "Do you want to trash this file?",
- None,
- &["Trash", "Cancel"],
- cx,
- );
+ if !entry.status.is_created() {
+ self.perform_checkout(vec![entry.repo_path.clone()], cx);
+ } else {
+ let prompt = prompt(&format!("Trash {}?", filename), None, window, cx);
cx.spawn_in(window, |_, mut cx| async move {
- match prompt.await {
- Ok(0) => {}
- _ => return Ok(()),
+ match prompt.await? {
+ TrashCancel::Trash => {}
+ TrashCancel::Cancel => return Ok(()),
}
let task = workspace.update(&mut cx, |workspace, cx| {
workspace
@@ -647,45 +680,235 @@ impl GitPanel {
cx,
|e, _, _| Some(format!("{e}")),
);
- return Some(());
}
+ Some(())
+ });
+ }
- let open_path = workspace.update(cx, |workspace, cx| {
- workspace.open_path_preview(path, None, true, false, window, cx)
- });
+ fn perform_checkout(&mut self, repo_paths: Vec<RepoPath>, cx: &mut Context<Self>) {
+ let workspace = self.workspace.clone();
+ let Some(active_repository) = self.active_repository.clone() else {
+ return;
+ };
- cx.spawn_in(window, |_, mut cx| async move {
- let item = open_path?.await?;
- let editor = cx.update(|_, cx| {
- item.act_as::<Editor>(cx)
- .ok_or_else(|| anyhow::anyhow!("didn't open editor"))
- })??;
+ let op_id = self.pending.iter().map(|p| p.op_id).max().unwrap_or(0) + 1;
+ self.pending.push(PendingOperation {
+ op_id,
+ target_status: TargetStatus::Reverted,
+ repo_paths: repo_paths.iter().cloned().collect(),
+ finished: false,
+ });
+ self.update_visible_entries(cx);
+ let task = cx.spawn(|_, mut cx| async move {
+ let tasks: Vec<_> = workspace.update(&mut cx, |workspace, cx| {
+ workspace.project().update(cx, |project, cx| {
+ repo_paths
+ .iter()
+ .filter_map(|repo_path| {
+ let path = active_repository
+ .read(cx)
+ .repo_path_to_project_path(&repo_path)?;
+ Some(project.open_buffer(path, cx))
+ })
+ .collect()
+ })
+ })?;
- if let Some(task) =
- editor.update(&mut cx, |editor, _| editor.wait_for_diff_to_load())?
- {
- task.await
- };
+ let buffers = futures::future::join_all(tasks).await;
- editor.update_in(&mut cx, |editor, window, cx| {
- editor.revert_file(&Default::default(), window, cx);
- })?;
+ active_repository
+ .update(&mut cx, |repo, _| repo.checkout_files("HEAD", repo_paths))?
+ .await??;
- workspace
- .update_in(&mut cx, |workspace, window, cx| {
- workspace.save_active_item(SaveIntent::Save, window, cx)
- })?
- .await?;
- Ok(())
+ let tasks: Vec<_> = cx.update(|cx| {
+ buffers
+ .iter()
+ .filter_map(|buffer| {
+ buffer.as_ref().ok()?.update(cx, |buffer, cx| {
+ buffer.is_dirty().then(|| buffer.reload(cx))
+ })
+ })
+ .collect()
+ })?;
+
+ futures::future::join_all(tasks).await;
+
+ Ok(())
+ });
+
+ cx.spawn(|this, mut cx| async move {
+ let result = task.await;
+
+ this.update(&mut cx, |this, cx| {
+ for pending in this.pending.iter_mut() {
+ if pending.op_id == op_id {
+ pending.finished = true;
+ if result.is_err() {
+ pending.target_status = TargetStatus::Unchanged;
+ this.update_visible_entries(cx);
+ }
+ break;
+ }
+ }
+ result
+ .map_err(|e| {
+ this.show_err_toast(e, cx);
+ })
+ .ok();
})
- .detach_and_prompt_err("Failed to open file", window, cx, |e, _, _| {
- Some(format!("{e}"))
- });
+ .ok();
+ })
+ .detach();
+ }
- Some(())
+ fn discard_tracked_changes(
+ &mut self,
+ _: &DiscardTrackedChanges,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let entries = self
+ .entries
+ .iter()
+ .filter_map(|entry| entry.status_entry().cloned())
+ .filter(|status_entry| !status_entry.status.is_created())
+ .collect::<Vec<_>>();
+
+ match entries.len() {
+ 0 => return,
+ 1 => return self.revert_entry(&entries[0], window, cx),
+ _ => {}
+ }
+ let details = entries
+ .iter()
+ .filter_map(|entry| entry.repo_path.0.file_name())
+ .map(|filename| filename.to_string_lossy())
+ .join("\n");
+
+ #[derive(strum::EnumIter, strum::VariantNames)]
+ #[strum(serialize_all = "title_case")]
+ enum DiscardCancel {
+ DiscardTrackedChanges,
+ Cancel,
+ }
+ let prompt = prompt(
+ "Discard changes to these files?",
+ Some(&details),
+ window,
+ cx,
+ );
+ cx.spawn(|this, mut cx| async move {
+ match prompt.await {
+ Ok(DiscardCancel::DiscardTrackedChanges) => {
+ this.update(&mut cx, |this, cx| {
+ let repo_paths = entries.into_iter().map(|entry| entry.repo_path).collect();
+ this.perform_checkout(repo_paths, cx);
+ })
+ .ok();
+ }
+ _ => {
+ return;
+ }
+ }
+ })
+ .detach();
+ }
+
+ fn clean_all(&mut self, _: &TrashUntrackedFiles, window: &mut Window, cx: &mut Context<Self>) {
+ let workspace = self.workspace.clone();
+ let Some(active_repo) = self.active_repository.clone() else {
+ return;
+ };
+ let to_delete = self
+ .entries
+ .iter()
+ .filter_map(|entry| entry.status_entry())
+ .filter(|status_entry| status_entry.status.is_created())
+ .cloned()
+ .collect::<Vec<_>>();
+
+ match to_delete.len() {
+ 0 => return,
+ 1 => return self.revert_entry(&to_delete[0], window, cx),
+ _ => {}
+ };
+
+ let details = to_delete
+ .iter()
+ .map(|entry| {
+ entry
+ .repo_path
+ .0
+ .file_name()
+ .map(|f| f.to_string_lossy())
+ .unwrap_or_default()
+ })
+ .join("\n");
+
+ let prompt = prompt("Trash these files?", Some(&details), window, cx);
+ cx.spawn_in(window, |this, mut cx| async move {
+ match prompt.await? {
+ TrashCancel::Trash => {}
+ TrashCancel::Cancel => return Ok(()),
+ }
+ let tasks = workspace.update(&mut cx, |workspace, cx| {
+ to_delete
+ .iter()
+ .filter_map(|entry| {
+ workspace.project().update(cx, |project, cx| {
+ let project_path = active_repo
+ .read(cx)
+ .repo_path_to_project_path(&entry.repo_path)?;
+ project.delete_file(project_path, true, cx)
+ })
+ })
+ .collect::<Vec<_>>()
+ })?;
+ let to_unstage = to_delete
+ .into_iter()
+ .filter_map(|entry| {
+ if entry.status.is_staged() != Some(false) {
+ Some(entry.repo_path.clone())
+ } else {
+ None
+ }
+ })
+ .collect();
+ this.update(&mut cx, |this, cx| {
+ this.perform_stage(false, to_unstage, cx)
+ })?;
+ for task in tasks {
+ task.await?;
+ }
+ Ok(())
+ })
+ .detach_and_prompt_err("Failed to trash files", window, cx, |e, _, _| {
+ Some(format!("{e}"))
});
}
+ fn stage_all(&mut self, _: &StageAll, _window: &mut Window, cx: &mut Context<Self>) {
+ let repo_paths = self
+ .entries
+ .iter()
+ .filter_map(|entry| entry.status_entry())
+ .filter(|status_entry| status_entry.is_staged != Some(true))
+ .map(|status_entry| status_entry.repo_path.clone())
+ .collect::<Vec<_>>();
+ self.perform_stage(true, repo_paths, cx);
+ }
+
+ fn unstage_all(&mut self, _: &UnstageAll, _window: &mut Window, cx: &mut Context<Self>) {
+ let repo_paths = self
+ .entries
+ .iter()
+ .filter_map(|entry| entry.status_entry())
+ .filter(|status_entry| status_entry.is_staged != Some(false))
+ .map(|status_entry| status_entry.repo_path.clone())
+ .collect::<Vec<_>>();
+ self.perform_stage(false, repo_paths, cx);
+ }
+
fn toggle_staged_for_entry(
&mut self,
entry: &GitListEntry,
@@ -720,22 +943,21 @@ impl GitPanel {
(goal_staged_state, entries)
}
};
- self.update_staging_area_for_entries(stage, repo_paths, cx);
+ self.perform_stage(stage, repo_paths, cx);
}
- fn update_staging_area_for_entries(
- &mut self,
- stage: bool,
- repo_paths: Vec<RepoPath>,
- cx: &mut Context<Self>,
- ) {
+ fn perform_stage(&mut self, stage: bool, repo_paths: Vec<RepoPath>, cx: &mut Context<Self>) {
let Some(active_repository) = self.active_repository.clone() else {
return;
};
let op_id = self.pending.iter().map(|p| p.op_id).max().unwrap_or(0) + 1;
self.pending.push(PendingOperation {
op_id,
- will_become_staged: stage,
+ target_status: if stage {
+ TargetStatus::Staged
+ } else {
+ TargetStatus::Unstaged
+ },
repo_paths: repo_paths.iter().cloned().collect(),
finished: false,
});
@@ -1105,6 +1327,14 @@ impl GitPanel {
let is_new = entry.status.is_created();
let is_staged = entry.status.is_staged();
+ if self.pending.iter().any(|pending| {
+ pending.target_status == TargetStatus::Reverted
+ && !pending.finished
+ && pending.repo_paths.contains(&entry.repo_path)
+ }) {
+ continue;
+ }
+
let display_name = if difference > 1 {
// Show partial path for deeply nested files
entry
@@ -1236,7 +1466,12 @@ impl GitPanel {
fn entry_is_staged(&self, entry: &GitStatusEntry) -> Option<bool> {
for pending in self.pending.iter().rev() {
if pending.repo_paths.contains(&entry.repo_path) {
- return Some(pending.will_become_staged);
+ match pending.target_status {
+ TargetStatus::Staged => return Some(true),
+ TargetStatus::Unstaged => return Some(false),
+ TargetStatus::Reverted => continue,
+ TargetStatus::Unchanged => continue,
+ }
}
}
entry.is_staged
@@ -1248,6 +1483,10 @@ impl GitPanel {
|| self.conflicted_staged_count > 0
}
+ fn has_conflicts(&self) -> bool {
+ self.conflicted_count > 0
+ }
+
fn has_tracked_changes(&self) -> bool {
self.tracked_count > 0
}
@@ -1316,10 +1555,17 @@ impl GitPanel {
.is_above_project()
});
- self.panel_header_container(window, cx)
- .when(all_repositories.len() > 1 || has_repo_above, |el| {
- el.child(self.render_repository_selector(cx))
- })
+ self.panel_header_container(window, cx).when(
+ all_repositories.len() > 1 || has_repo_above,
+ |el| {
+ el.child(
+ Label::new("Repository")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .child(self.render_repository_selector(cx))
+ },
+ )
}
pub fn render_repository_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
@@ -1701,6 +1947,12 @@ impl GitPanel {
.with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
.track_scroll(self.scroll_handle.clone()),
)
+ .on_mouse_down(
+ MouseButton::Right,
+ cx.listener(move |this, event: &MouseDownEvent, window, cx| {
+ this.deploy_panel_context_menu(event.position, window, cx)
+ }),
+ )
.children(self.render_scrollbar(cx))
}
@@ -1743,7 +1995,7 @@ impl GitPanel {
repo.update(cx, |repo, cx| repo.show(sha, cx))
}
- fn deploy_context_menu(
+ fn deploy_entry_context_menu(
&mut self,
position: Point<Pixels>,
ix: usize,
@@ -1768,7 +2020,38 @@ impl GitPanel {
.action("Open Diff", Confirm.boxed_clone())
.action("Open File", SecondaryConfirm.boxed_clone())
});
+ self.selected_entry = Some(ix);
+ self.set_context_menu(context_menu, position, window, cx);
+ }
+
+ fn deploy_panel_context_menu(
+ &mut self,
+ position: Point<Pixels>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let context_menu = ContextMenu::build(window, cx, |context_menu, _, _| {
+ context_menu
+ .action("Stage All", StageAll.boxed_clone())
+ .action("Unstage All", UnstageAll.boxed_clone())
+ .action("Open Diff", project_diff::Diff.boxed_clone())
+ .separator()
+ .action(
+ "Discard Tracked Changes",
+ DiscardTrackedChanges.boxed_clone(),
+ )
+ .action("Trash Untracked Files", TrashUntrackedFiles.boxed_clone())
+ });
+ self.set_context_menu(context_menu, position, window, cx);
+ }
+ fn set_context_menu(
+ &mut self,
+ context_menu: Entity<ContextMenu>,
+ position: Point<Pixels>,
+ window: &Window,
+ cx: &mut Context<Self>,
+ ) {
let subscription = cx.subscribe_in(
&context_menu,
window,
@@ -1782,7 +2065,6 @@ impl GitPanel {
cx.notify();
},
);
- self.selected_entry = Some(ix);
self.context_menu = Some((context_menu, position, subscription));
cx.notify();
}
@@ -1834,14 +2116,14 @@ impl GitPanel {
let mut is_staged: ToggleState = self.entry_is_staged(entry).into();
- if !self.has_staged_changes() && !entry.status.is_created() {
+ if !self.has_staged_changes() && !self.has_conflicts() && !entry.status.is_created() {
is_staged = ToggleState::Selected;
}
let checkbox = Checkbox::new(id, is_staged)
.disabled(!has_write_access)
.fill()
- .placeholder(!self.has_staged_changes())
+ .placeholder(!self.has_staged_changes() && !self.has_conflicts())
.elevation(ElevationIndex::Surface)
.on_click({
let entry = entry.clone();
@@ -1888,7 +2170,8 @@ impl GitPanel {
})
.on_secondary_mouse_down(cx.listener(
move |this, event: &MouseDownEvent, window, cx| {
- this.deploy_context_menu(event.position, ix, window, cx)
+ this.deploy_entry_context_menu(event.position, ix, window, cx);
+ cx.stop_propagation();
},
))
.child(
@@ -1921,12 +2204,7 @@ impl GitPanel {
impl Render for GitPanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let project = self.project.read(cx);
- let has_entries = self
- .active_repository
- .as_ref()
- .map_or(false, |active_repository| {
- active_repository.read(cx).entry_count() > 0
- });
+ let has_entries = self.entries.len() > 0;
let room = self
.workspace
.upgrade()
@@ -1959,10 +2237,14 @@ impl Render for GitPanel {
.on_action(cx.listener(Self::close_panel))
.on_action(cx.listener(Self::open_diff))
.on_action(cx.listener(Self::open_file))
- .on_action(cx.listener(Self::revert))
+ .on_action(cx.listener(Self::revert_selected))
.on_action(cx.listener(Self::focus_changes_list))
.on_action(cx.listener(Self::focus_editor))
.on_action(cx.listener(Self::toggle_staged_for_selected))
+ .on_action(cx.listener(Self::stage_all))
+ .on_action(cx.listener(Self::unstage_all))
+ .on_action(cx.listener(Self::discard_tracked_changes))
+ .on_action(cx.listener(Self::clean_all))
.when(has_write_access && has_co_authors, |git_panel| {
git_panel.on_action(cx.listener(Self::toggle_fill_co_authors))
})
@@ -65,6 +65,11 @@ pub enum Message {
commit: SharedString,
reset_mode: ResetMode,
},
+ CheckoutFiles {
+ repo: GitRepo,
+ commit: SharedString,
+ paths: Vec<RepoPath>,
+ },
Stage(GitRepo, Vec<RepoPath>),
Unstage(GitRepo, Vec<RepoPath>),
SetIndexText(GitRepo, RepoPath, Option<String>),
@@ -106,6 +111,7 @@ impl GitStore {
client.add_entity_request_handler(Self::handle_commit);
client.add_entity_request_handler(Self::handle_reset);
client.add_entity_request_handler(Self::handle_show);
+ client.add_entity_request_handler(Self::handle_checkout_files);
client.add_entity_request_handler(Self::handle_open_commit_message_buffer);
client.add_entity_request_handler(Self::handle_set_index_text);
}
@@ -121,8 +127,6 @@ impl GitStore {
event: &WorktreeStoreEvent,
cx: &mut Context<'_, Self>,
) {
- // TODO inspect the event
-
let mut new_repositories = Vec::new();
let mut new_active_index = None;
let this = cx.weak_entity();
@@ -282,6 +286,36 @@ impl GitStore {
}
Ok(())
}
+
+ Message::CheckoutFiles {
+ repo,
+ commit,
+ paths,
+ } => {
+ match repo {
+ GitRepo::Local(repo) => repo.checkout_files(&commit, &paths)?,
+ GitRepo::Remote {
+ project_id,
+ client,
+ worktree_id,
+ work_directory_id,
+ } => {
+ client
+ .request(proto::GitCheckoutFiles {
+ project_id: project_id.0,
+ worktree_id: worktree_id.to_proto(),
+ work_directory_id: work_directory_id.to_proto(),
+ commit: commit.into(),
+ paths: paths
+ .into_iter()
+ .map(|p| p.to_string_lossy().to_string())
+ .collect(),
+ })
+ .await?;
+ }
+ }
+ Ok(())
+ }
Message::Unstage(repo, paths) => {
match repo {
GitRepo::Local(repo) => repo.unstage_paths(&paths)?,
@@ -502,6 +536,30 @@ impl GitStore {
Ok(proto::Ack {})
}
+ async fn handle_checkout_files(
+ this: Entity<Self>,
+ envelope: TypedEnvelope<proto::GitCheckoutFiles>,
+ mut cx: AsyncApp,
+ ) -> Result<proto::Ack> {
+ let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+ let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
+ let repository_handle =
+ Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
+ let paths = envelope
+ .payload
+ .paths
+ .iter()
+ .map(|s| RepoPath::from_str(s))
+ .collect();
+
+ repository_handle
+ .update(&mut cx, |repository_handle, _| {
+ repository_handle.checkout_files(&envelope.payload.commit, paths)
+ })?
+ .await??;
+ Ok(proto::Ack {})
+ }
+
async fn handle_open_commit_message_buffer(
this: Entity<Self>,
envelope: TypedEnvelope<proto::OpenCommitMessageBuffer>,
@@ -712,6 +770,26 @@ impl Repository {
})
}
+ pub fn checkout_files(
+ &self,
+ commit: &str,
+ paths: Vec<RepoPath>,
+ ) -> oneshot::Receiver<Result<()>> {
+ let (result_tx, result_rx) = futures::channel::oneshot::channel();
+ let commit = commit.to_string().into();
+ self.update_sender
+ .unbounded_send((
+ Message::CheckoutFiles {
+ repo: self.git_repo.clone(),
+ commit,
+ paths,
+ },
+ result_tx,
+ ))
+ .ok();
+ result_rx
+ }
+
pub fn reset(&self, commit: &str, reset_mode: ResetMode) -> oneshot::Receiver<Result<()>> {
let (result_tx, result_rx) = futures::channel::oneshot::channel();
let commit = commit.to_string().into();
@@ -320,7 +320,8 @@ message Envelope {
GitReset git_reset = 301;
GitCommitDetails git_commit_details = 302;
- SetIndexText set_index_text = 299; // current max
+ SetIndexText set_index_text = 299;
+ GitCheckoutFiles git_checkout_files = 303; // current max
}
reserved 87 to 88;
@@ -2688,6 +2689,14 @@ message GitReset {
}
}
+message GitCheckoutFiles {
+ uint64 project_id = 1;
+ uint64 worktree_id = 2;
+ uint64 work_directory_id = 3;
+ string commit = 4;
+ repeated string paths = 5;
+}
+
message GetPanicFilesResponse {
repeated string file_contents = 2;
}
@@ -441,6 +441,7 @@ messages!(
(InstallExtension, Background),
(RegisterBufferWithLanguageServers, Background),
(GitReset, Background),
+ (GitCheckoutFiles, Background),
(GitShow, Background),
(GitCommitDetails, Background),
(SetIndexText, Background),
@@ -579,6 +580,7 @@ request_messages!(
(RegisterBufferWithLanguageServers, Ack),
(GitShow, GitCommitDetails),
(GitReset, Ack),
+ (GitCheckoutFiles, Ack),
(SetIndexText, Ack),
);
@@ -674,6 +676,7 @@ entity_messages!(
RegisterBufferWithLanguageServers,
GitShow,
GitReset,
+ GitCheckoutFiles,
SetIndexText,
);