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:
parent
bd105a5fc7
commit
be83074243
9 changed files with 488 additions and 82 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -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(());
|
||||
}
|
||||
|
||||
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))
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue