Git context menu (#24844)

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
This commit is contained in:
Conrad Irwin 2025-02-14 14:04:32 -07:00 committed by GitHub
parent bd105a5fc7
commit be83074243
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 488 additions and 82 deletions

1
Cargo.lock generated
View file

@ -5370,6 +5370,7 @@ dependencies = [
"serde_derive",
"serde_json",
"settings",
"strum",
"theme",
"time",
"ui",

View file

@ -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>)

View file

@ -37,7 +37,8 @@ actions!(
// editor::RevertSelectedHunks
StageAll,
UnstageAll,
RevertAll,
DiscardTrackedChanges,
TrashUntrackedFiles,
Uncommit,
Commit,
ClearCommitMessage

View file

@ -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()

View file

@ -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

View file

@ -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(());
}
let open_path = workspace.update(cx, |workspace, cx| {
workspace.open_path_preview(path, None, true, false, window, cx)
});
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"))
})??;
if let Some(task) =
editor.update(&mut cx, |editor, _| editor.wait_for_diff_to_load())?
{
task.await
};
editor.update_in(&mut cx, |editor, window, cx| {
editor.revert_file(&Default::default(), window, cx);
})?;
workspace
.update_in(&mut cx, |workspace, window, cx| {
workspace.save_active_item(SaveIntent::Save, window, cx)
})?
.await?;
Ok(())
})
.detach_and_prompt_err("Failed to open file", window, cx, |e, _, _| {
Some(format!("{e}"))
});
Some(())
});
}
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;
};
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()
})
})?;
let buffers = futures::future::join_all(tasks).await;
active_repository
.update(&mut cx, |repo, _| repo.checkout_files("HEAD", repo_paths))?
.await??;
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();
})
.ok();
})
.detach();
}
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))
})

View file

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

View file

@ -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;
}

View file

@ -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,
);