git: Implement commit creation (#23263)
- [x] Basic implementation - [x] Disable commit buttons when committing is not possible (empty message, no changes) - [x] Upgrade GitSummary to efficiently figure out whether there are any staged changes - [x] Make CommitAll work - [x] Surface errors with toasts - [x] Channel shutdown - [x] Empty commit message or no changes - [x] Failed git operations - [x] Fix added files no longer appearing correctly in the project panel (GitSummary breakage) - [x] Fix handling of commit message Release Notes: - N/A --------- Co-authored-by: Nate <nate@zed.dev>
This commit is contained in:
parent
3767e7e5f0
commit
5da67899b7
10 changed files with 387 additions and 198 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -5192,6 +5192,7 @@ dependencies = [
|
||||||
"collections",
|
"collections",
|
||||||
"db",
|
"db",
|
||||||
"editor",
|
"editor",
|
||||||
|
"futures 0.3.31",
|
||||||
"git",
|
"git",
|
||||||
"gpui",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
|
|
|
@ -1560,13 +1560,14 @@ pub fn entry_diagnostic_aware_icon_decoration_and_color(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn entry_git_aware_label_color(git_status: GitSummary, ignored: bool, selected: bool) -> Color {
|
pub fn entry_git_aware_label_color(git_status: GitSummary, ignored: bool, selected: bool) -> Color {
|
||||||
|
let tracked = git_status.index + git_status.worktree;
|
||||||
if ignored {
|
if ignored {
|
||||||
Color::Ignored
|
Color::Ignored
|
||||||
} else if git_status.conflict > 0 {
|
} else if git_status.conflict > 0 {
|
||||||
Color::Conflict
|
Color::Conflict
|
||||||
} else if git_status.modified > 0 {
|
} else if tracked.modified > 0 {
|
||||||
Color::Modified
|
Color::Modified
|
||||||
} else if git_status.added > 0 || git_status.untracked > 0 {
|
} else if tracked.added > 0 || git_status.untracked > 0 {
|
||||||
Color::Created
|
Color::Created
|
||||||
} else {
|
} else {
|
||||||
entry_label_color(selected)
|
entry_label_color(selected)
|
||||||
|
|
|
@ -61,6 +61,8 @@ pub trait GitRepository: Send + Sync {
|
||||||
///
|
///
|
||||||
/// If any of the paths were previously staged but do not exist in HEAD, they will be removed from the index.
|
/// If any of the paths were previously staged but do not exist in HEAD, they will be removed from the index.
|
||||||
fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()>;
|
fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()>;
|
||||||
|
|
||||||
|
fn commit(&self, message: &str) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for dyn GitRepository {
|
impl std::fmt::Debug for dyn GitRepository {
|
||||||
|
@ -280,6 +282,24 @@ impl GitRepository for RealGitRepository {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn commit(&self, message: &str) -> Result<()> {
|
||||||
|
let working_directory = self
|
||||||
|
.repository
|
||||||
|
.lock()
|
||||||
|
.workdir()
|
||||||
|
.context("failed to read git work directory")?
|
||||||
|
.to_path_buf();
|
||||||
|
|
||||||
|
let cmd = new_std_command(&self.git_binary_path)
|
||||||
|
.current_dir(&working_directory)
|
||||||
|
.args(["commit", "--quiet", "-m", message])
|
||||||
|
.status()?;
|
||||||
|
if !cmd.success() {
|
||||||
|
return Err(anyhow!("Failed to commit: {cmd}"));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -423,6 +443,10 @@ impl GitRepository for FakeGitRepository {
|
||||||
fn unstage_paths(&self, _paths: &[RepoPath]) -> Result<()> {
|
fn unstage_paths(&self, _paths: &[RepoPath]) -> Result<()> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn commit(&self, _message: &str) -> Result<()> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
|
fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
|
||||||
|
|
|
@ -171,13 +171,13 @@ impl FileStatus {
|
||||||
FileStatus::Tracked(TrackedStatus {
|
FileStatus::Tracked(TrackedStatus {
|
||||||
index_status,
|
index_status,
|
||||||
worktree_status,
|
worktree_status,
|
||||||
}) => {
|
}) => GitSummary {
|
||||||
let mut summary = index_status.to_summary() + worktree_status.to_summary();
|
index: index_status.to_summary(),
|
||||||
if summary != GitSummary::UNCHANGED {
|
worktree: worktree_status.to_summary(),
|
||||||
summary.count = 1;
|
conflict: 0,
|
||||||
};
|
untracked: 0,
|
||||||
summary
|
count: 1,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -196,28 +196,39 @@ impl StatusCode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the contribution of this status code to the Git summary.
|
fn to_summary(self) -> TrackedSummary {
|
||||||
///
|
|
||||||
/// Note that this does not include the count field, which must be set manually.
|
|
||||||
fn to_summary(self) -> GitSummary {
|
|
||||||
match self {
|
match self {
|
||||||
StatusCode::Modified | StatusCode::TypeChanged => GitSummary {
|
StatusCode::Modified | StatusCode::TypeChanged => TrackedSummary {
|
||||||
modified: 1,
|
modified: 1,
|
||||||
..GitSummary::UNCHANGED
|
..TrackedSummary::UNCHANGED
|
||||||
},
|
},
|
||||||
StatusCode::Added => GitSummary {
|
StatusCode::Added => TrackedSummary {
|
||||||
added: 1,
|
added: 1,
|
||||||
..GitSummary::UNCHANGED
|
..TrackedSummary::UNCHANGED
|
||||||
},
|
},
|
||||||
StatusCode::Deleted => GitSummary {
|
StatusCode::Deleted => TrackedSummary {
|
||||||
deleted: 1,
|
deleted: 1,
|
||||||
..GitSummary::UNCHANGED
|
..TrackedSummary::UNCHANGED
|
||||||
},
|
},
|
||||||
StatusCode::Renamed | StatusCode::Copied | StatusCode::Unmodified => {
|
StatusCode::Renamed | StatusCode::Copied | StatusCode::Unmodified => {
|
||||||
GitSummary::UNCHANGED
|
TrackedSummary::UNCHANGED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn index(self) -> FileStatus {
|
||||||
|
FileStatus::Tracked(TrackedStatus {
|
||||||
|
index_status: self,
|
||||||
|
worktree_status: StatusCode::Unmodified,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn worktree(self) -> FileStatus {
|
||||||
|
FileStatus::Tracked(TrackedStatus {
|
||||||
|
index_status: StatusCode::Unmodified,
|
||||||
|
worktree_status: self,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UnmergedStatusCode {
|
impl UnmergedStatusCode {
|
||||||
|
@ -232,12 +243,76 @@ impl UnmergedStatusCode {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)]
|
#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)]
|
||||||
pub struct GitSummary {
|
pub struct TrackedSummary {
|
||||||
pub added: usize,
|
pub added: usize,
|
||||||
pub modified: usize,
|
pub modified: usize,
|
||||||
|
pub deleted: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrackedSummary {
|
||||||
|
pub const UNCHANGED: Self = Self {
|
||||||
|
added: 0,
|
||||||
|
modified: 0,
|
||||||
|
deleted: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ADDED: Self = Self {
|
||||||
|
added: 1,
|
||||||
|
modified: 0,
|
||||||
|
deleted: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const MODIFIED: Self = Self {
|
||||||
|
added: 0,
|
||||||
|
modified: 1,
|
||||||
|
deleted: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const DELETED: Self = Self {
|
||||||
|
added: 0,
|
||||||
|
modified: 0,
|
||||||
|
deleted: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::AddAssign for TrackedSummary {
|
||||||
|
fn add_assign(&mut self, rhs: Self) {
|
||||||
|
self.added += rhs.added;
|
||||||
|
self.modified += rhs.modified;
|
||||||
|
self.deleted += rhs.deleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Add for TrackedSummary {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, rhs: Self) -> Self::Output {
|
||||||
|
TrackedSummary {
|
||||||
|
added: self.added + rhs.added,
|
||||||
|
modified: self.modified + rhs.modified,
|
||||||
|
deleted: self.deleted + rhs.deleted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Sub for TrackedSummary {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn sub(self, rhs: Self) -> Self::Output {
|
||||||
|
TrackedSummary {
|
||||||
|
added: self.added - rhs.added,
|
||||||
|
modified: self.modified - rhs.modified,
|
||||||
|
deleted: self.deleted - rhs.deleted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)]
|
||||||
|
pub struct GitSummary {
|
||||||
|
pub index: TrackedSummary,
|
||||||
|
pub worktree: TrackedSummary,
|
||||||
pub conflict: usize,
|
pub conflict: usize,
|
||||||
pub untracked: usize,
|
pub untracked: usize,
|
||||||
pub deleted: usize,
|
|
||||||
pub count: usize,
|
pub count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,11 +330,10 @@ impl GitSummary {
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const UNCHANGED: Self = Self {
|
pub const UNCHANGED: Self = Self {
|
||||||
added: 0,
|
index: TrackedSummary::UNCHANGED,
|
||||||
modified: 0,
|
worktree: TrackedSummary::UNCHANGED,
|
||||||
conflict: 0,
|
conflict: 0,
|
||||||
untracked: 0,
|
untracked: 0,
|
||||||
deleted: 0,
|
|
||||||
count: 0,
|
count: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -293,11 +367,10 @@ impl std::ops::Add<Self> for GitSummary {
|
||||||
|
|
||||||
impl std::ops::AddAssign for GitSummary {
|
impl std::ops::AddAssign for GitSummary {
|
||||||
fn add_assign(&mut self, rhs: Self) {
|
fn add_assign(&mut self, rhs: Self) {
|
||||||
self.added += rhs.added;
|
self.index += rhs.index;
|
||||||
self.modified += rhs.modified;
|
self.worktree += rhs.worktree;
|
||||||
self.conflict += rhs.conflict;
|
self.conflict += rhs.conflict;
|
||||||
self.untracked += rhs.untracked;
|
self.untracked += rhs.untracked;
|
||||||
self.deleted += rhs.deleted;
|
|
||||||
self.count += rhs.count;
|
self.count += rhs.count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -307,11 +380,10 @@ impl std::ops::Sub for GitSummary {
|
||||||
|
|
||||||
fn sub(self, rhs: Self) -> Self::Output {
|
fn sub(self, rhs: Self) -> Self::Output {
|
||||||
GitSummary {
|
GitSummary {
|
||||||
added: self.added - rhs.added,
|
index: self.index - rhs.index,
|
||||||
modified: self.modified - rhs.modified,
|
worktree: self.worktree - rhs.worktree,
|
||||||
conflict: self.conflict - rhs.conflict,
|
conflict: self.conflict - rhs.conflict,
|
||||||
untracked: self.untracked - rhs.untracked,
|
untracked: self.untracked - rhs.untracked,
|
||||||
deleted: self.deleted - rhs.deleted,
|
|
||||||
count: self.count - rhs.count,
|
count: self.count - rhs.count,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ anyhow.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
db.workspace = true
|
db.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
git.workspace = true
|
git.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
|
|
|
@ -4,6 +4,8 @@ use anyhow::{Context as _, Result};
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use editor::scroll::ScrollbarAutoHide;
|
use editor::scroll::ScrollbarAutoHide;
|
||||||
use editor::{Editor, EditorSettings, ShowScrollbar};
|
use editor::{Editor, EditorSettings, ShowScrollbar};
|
||||||
|
use futures::channel::mpsc;
|
||||||
|
use futures::StreamExt as _;
|
||||||
use git::repository::{GitRepository, RepoPath};
|
use git::repository::{GitRepository, RepoPath};
|
||||||
use git::status::FileStatus;
|
use git::status::FileStatus;
|
||||||
use git::{CommitAllChanges, CommitChanges, RevertAll, StageAll, ToggleStaged, UnstageAll};
|
use git::{CommitAllChanges, CommitChanges, RevertAll, StageAll, ToggleStaged, UnstageAll};
|
||||||
|
@ -21,7 +23,8 @@ use ui::{
|
||||||
prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, Scrollbar, ScrollbarState, Tooltip,
|
prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, Scrollbar, ScrollbarState, Tooltip,
|
||||||
};
|
};
|
||||||
use util::{maybe, ResultExt, TryFutureExt};
|
use util::{maybe, ResultExt, TryFutureExt};
|
||||||
use workspace::notifications::DetachAndPromptErr;
|
use workspace::notifications::{DetachAndPromptErr, NotificationId};
|
||||||
|
use workspace::Toast;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
dock::{DockPosition, Panel, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
Workspace,
|
Workspace,
|
||||||
|
@ -76,6 +79,7 @@ pub struct GitListEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GitPanel {
|
pub struct GitPanel {
|
||||||
|
weak_workspace: WeakView<Workspace>,
|
||||||
current_modifiers: Modifiers,
|
current_modifiers: Modifiers,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
|
@ -92,6 +96,7 @@ pub struct GitPanel {
|
||||||
all_staged: Option<bool>,
|
all_staged: Option<bool>,
|
||||||
width: Option<Pixels>,
|
width: Option<Pixels>,
|
||||||
reveal_in_editor: Task<()>,
|
reveal_in_editor: Task<()>,
|
||||||
|
err_sender: mpsc::Sender<anyhow::Error>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn first_worktree_repository(
|
fn first_worktree_repository(
|
||||||
|
@ -143,11 +148,14 @@ impl GitPanel {
|
||||||
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
|
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
|
||||||
let fs = workspace.app_state().fs.clone();
|
let fs = workspace.app_state().fs.clone();
|
||||||
let project = workspace.project().clone();
|
let project = workspace.project().clone();
|
||||||
|
let weak_workspace = cx.view().downgrade();
|
||||||
let git_state = project.read(cx).git_state().cloned();
|
let git_state = project.read(cx).git_state().cloned();
|
||||||
let language_registry = workspace.app_state().languages.clone();
|
let language_registry = workspace.app_state().languages.clone();
|
||||||
let current_commit_message = git_state
|
let current_commit_message = git_state
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|git_state| git_state.read(cx).commit_message.clone());
|
.map(|git_state| git_state.read(cx).commit_message.clone());
|
||||||
|
|
||||||
|
let (err_sender, mut err_receiver) = mpsc::channel(1);
|
||||||
|
|
||||||
let git_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
|
let git_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
|
||||||
let focus_handle = cx.focus_handle();
|
let focus_handle = cx.focus_handle();
|
||||||
|
@ -319,6 +327,7 @@ impl GitPanel {
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
let mut git_panel = Self {
|
let mut git_panel = Self {
|
||||||
|
weak_workspace,
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
fs,
|
fs,
|
||||||
pending_serialization: Task::ready(None),
|
pending_serialization: Task::ready(None),
|
||||||
|
@ -333,14 +342,33 @@ impl GitPanel {
|
||||||
hide_scrollbar_task: None,
|
hide_scrollbar_task: None,
|
||||||
rebuild_requested,
|
rebuild_requested,
|
||||||
commit_editor,
|
commit_editor,
|
||||||
reveal_in_editor: Task::ready(()),
|
|
||||||
project,
|
project,
|
||||||
|
reveal_in_editor: Task::ready(()),
|
||||||
|
err_sender,
|
||||||
};
|
};
|
||||||
git_panel.schedule_update();
|
git_panel.schedule_update();
|
||||||
git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx);
|
git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx);
|
||||||
git_panel
|
git_panel
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let handle = git_panel.downgrade();
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
while let Some(e) = err_receiver.next().await {
|
||||||
|
let Some(this) = handle.upgrade() else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
if this
|
||||||
|
.update(&mut cx, |this, cx| {
|
||||||
|
this.show_err_toast("git operation error", e, cx);
|
||||||
|
})
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
cx.subscribe(
|
cx.subscribe(
|
||||||
&git_panel,
|
&git_panel,
|
||||||
move |workspace, _, event: &Event, cx| match event.clone() {
|
move |workspace, _, event: &Event, cx| match event.clone() {
|
||||||
|
@ -606,13 +634,16 @@ impl GitPanel {
|
||||||
let Some(git_state) = self.git_state(cx) else {
|
let Some(git_state) = self.git_state(cx) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
git_state.update(cx, |git_state, _| {
|
let result = git_state.update(cx, |git_state, _| {
|
||||||
if entry.status.is_staged().unwrap_or(false) {
|
if entry.status.is_staged().unwrap_or(false) {
|
||||||
git_state.stage_entries(vec![entry.repo_path.clone()]);
|
git_state.unstage_entries(vec![entry.repo_path.clone()], self.err_sender.clone())
|
||||||
} else {
|
} else {
|
||||||
git_state.stage_entries(vec![entry.repo_path.clone()]);
|
git_state.stage_entries(vec![entry.repo_path.clone()], self.err_sender.clone())
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if let Err(e) = result {
|
||||||
|
self.show_err_toast("toggle staged error", e, cx);
|
||||||
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -649,7 +680,10 @@ impl GitPanel {
|
||||||
entry.is_staged = Some(true);
|
entry.is_staged = Some(true);
|
||||||
}
|
}
|
||||||
self.all_staged = Some(true);
|
self.all_staged = Some(true);
|
||||||
git_state.read(cx).stage_all();
|
|
||||||
|
if let Err(e) = git_state.read(cx).stage_all(self.err_sender.clone()) {
|
||||||
|
self.show_err_toast("stage all error", e, cx);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unstage_all(&mut self, _: &git::UnstageAll, cx: &mut ViewContext<Self>) {
|
fn unstage_all(&mut self, _: &git::UnstageAll, cx: &mut ViewContext<Self>) {
|
||||||
|
@ -660,7 +694,9 @@ impl GitPanel {
|
||||||
entry.is_staged = Some(false);
|
entry.is_staged = Some(false);
|
||||||
}
|
}
|
||||||
self.all_staged = Some(false);
|
self.all_staged = Some(false);
|
||||||
git_state.read(cx).unstage_all();
|
if let Err(e) = git_state.read(cx).unstage_all(self.err_sender.clone()) {
|
||||||
|
self.show_err_toast("unstage all error", e, cx);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn discard_all(&mut self, _: &git::RevertAll, _cx: &mut ViewContext<Self>) {
|
fn discard_all(&mut self, _: &git::RevertAll, _cx: &mut ViewContext<Self>) {
|
||||||
|
@ -668,53 +704,32 @@ impl GitPanel {
|
||||||
println!("Discard all triggered");
|
println!("Discard all triggered");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_message(&mut self, cx: &mut ViewContext<Self>) {
|
/// Commit all staged changes
|
||||||
|
fn commit_changes(&mut self, _: &git::CommitChanges, cx: &mut ViewContext<Self>) {
|
||||||
let Some(git_state) = self.git_state(cx) else {
|
let Some(git_state) = self.git_state(cx) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
git_state.update(cx, |git_state, _| {
|
if let Err(e) =
|
||||||
git_state.clear_commit_message();
|
git_state.update(cx, |git_state, _| git_state.commit(self.err_sender.clone()))
|
||||||
});
|
{
|
||||||
|
self.show_err_toast("commit error", e, cx);
|
||||||
|
};
|
||||||
self.commit_editor
|
self.commit_editor
|
||||||
.update(cx, |editor, cx| editor.set_text("", cx));
|
.update(cx, |editor, cx| editor.set_text("", cx));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn can_commit(&self, commit_all: bool, cx: &AppContext) -> bool {
|
|
||||||
let Some(git_state) = self.git_state(cx) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
let has_message = !self.commit_editor.read(cx).text(cx).is_empty();
|
|
||||||
let has_changes = git_state.read(cx).entry_count() > 0;
|
|
||||||
let has_staged_changes = self
|
|
||||||
.visible_entries
|
|
||||||
.iter()
|
|
||||||
.any(|entry| entry.is_staged == Some(true));
|
|
||||||
|
|
||||||
has_message && (commit_all || has_staged_changes) && has_changes
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Commit all staged changes
|
|
||||||
fn commit_changes(&mut self, _: &git::CommitChanges, cx: &mut ViewContext<Self>) {
|
|
||||||
self.clear_message(cx);
|
|
||||||
|
|
||||||
if !self.can_commit(false, cx) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Implement commit all staged
|
|
||||||
println!("Commit staged changes triggered");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Commit all changes, regardless of whether they are staged or not
|
/// Commit all changes, regardless of whether they are staged or not
|
||||||
fn commit_all_changes(&mut self, _: &git::CommitAllChanges, cx: &mut ViewContext<Self>) {
|
fn commit_all_changes(&mut self, _: &git::CommitAllChanges, cx: &mut ViewContext<Self>) {
|
||||||
self.clear_message(cx);
|
let Some(git_state) = self.git_state(cx) else {
|
||||||
|
|
||||||
if !self.can_commit(true, cx) {
|
|
||||||
return;
|
return;
|
||||||
}
|
};
|
||||||
|
if let Err(e) = git_state.update(cx, |git_state, _| {
|
||||||
// TODO: Implement commit all changes
|
git_state.commit_all(self.err_sender.clone())
|
||||||
println!("Commit all changes triggered");
|
}) {
|
||||||
|
self.show_err_toast("commit all error", e, cx);
|
||||||
|
};
|
||||||
|
self.commit_editor
|
||||||
|
.update(cx, |editor, cx| editor.set_text("", cx));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn no_entries(&self, cx: &mut ViewContext<Self>) -> bool {
|
fn no_entries(&self, cx: &mut ViewContext<Self>) -> bool {
|
||||||
|
@ -840,12 +855,26 @@ impl GitPanel {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
git_state.update(cx, |git_state, _| {
|
git_state.update(cx, |git_state, _| {
|
||||||
git_state.commit_message = Some(commit_message.into())
|
git_state.commit_message = commit_message.into();
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn show_err_toast(&self, id: &'static str, e: anyhow::Error, cx: &mut ViewContext<Self>) {
|
||||||
|
let Some(workspace) = self.weak_workspace.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let notif_id = NotificationId::Named(id.into());
|
||||||
|
let message = e.to_string();
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
let toast = Toast::new(notif_id, message).on_click("Open Zed Log", |cx| {
|
||||||
|
cx.dispatch_action(workspace::OpenLog.boxed_clone());
|
||||||
|
});
|
||||||
|
workspace.show_toast(toast, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GitPanel –– Render
|
// GitPanel –– Render
|
||||||
|
@ -989,6 +1018,10 @@ impl GitPanel {
|
||||||
pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
|
pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
|
||||||
let editor = self.commit_editor.clone();
|
let editor = self.commit_editor.clone();
|
||||||
let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
|
let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
|
||||||
|
let (can_commit, can_commit_all) = self.git_state(cx).map_or((false, false), |git_state| {
|
||||||
|
let git_state = git_state.read(cx);
|
||||||
|
(git_state.can_commit(false), git_state.can_commit(true))
|
||||||
|
});
|
||||||
|
|
||||||
let focus_handle_1 = self.focus_handle(cx).clone();
|
let focus_handle_1 = self.focus_handle(cx).clone();
|
||||||
let focus_handle_2 = self.focus_handle(cx).clone();
|
let focus_handle_2 = self.focus_handle(cx).clone();
|
||||||
|
@ -1004,6 +1037,7 @@ impl GitPanel {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
.disabled(!can_commit)
|
||||||
.on_click(
|
.on_click(
|
||||||
cx.listener(|this, _: &ClickEvent, cx| this.commit_changes(&CommitChanges, cx)),
|
cx.listener(|this, _: &ClickEvent, cx| this.commit_changes(&CommitChanges, cx)),
|
||||||
);
|
);
|
||||||
|
@ -1019,6 +1053,7 @@ impl GitPanel {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
.disabled(!can_commit_all)
|
||||||
.on_click(cx.listener(|this, _: &ClickEvent, cx| {
|
.on_click(cx.listener(|this, _: &ClickEvent, cx| {
|
||||||
this.commit_all_changes(&CommitAllChanges, cx)
|
this.commit_all_changes(&CommitAllChanges, cx)
|
||||||
}));
|
}));
|
||||||
|
@ -1243,14 +1278,15 @@ impl GitPanel {
|
||||||
let Some(git_state) = this.git_state(cx) else {
|
let Some(git_state) = this.git_state(cx) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
git_state.update(cx, |git_state, _| match toggle {
|
let result = git_state.update(cx, |git_state, _| match toggle {
|
||||||
ToggleState::Selected | ToggleState::Indeterminate => {
|
ToggleState::Selected | ToggleState::Indeterminate => git_state
|
||||||
git_state.stage_entries(vec![repo_path]);
|
.stage_entries(vec![repo_path], this.err_sender.clone()),
|
||||||
}
|
ToggleState::Unselected => git_state
|
||||||
ToggleState::Unselected => {
|
.unstage_entries(vec![repo_path], this.err_sender.clone()),
|
||||||
git_state.unstage_entries(vec![repo_path])
|
});
|
||||||
}
|
if let Err(e) = result {
|
||||||
})
|
this.show_err_toast("toggle staged error", e, cx);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,52 +1,65 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
use futures::channel::mpsc;
|
use futures::channel::mpsc;
|
||||||
use futures::StreamExt as _;
|
use futures::{SinkExt as _, StreamExt as _};
|
||||||
use git::repository::{GitRepository, RepoPath};
|
use git::{
|
||||||
|
repository::{GitRepository, RepoPath},
|
||||||
|
status::{GitSummary, TrackedSummary},
|
||||||
|
};
|
||||||
use gpui::{AppContext, SharedString};
|
use gpui::{AppContext, SharedString};
|
||||||
use settings::WorktreeId;
|
use settings::WorktreeId;
|
||||||
use util::ResultExt as _;
|
|
||||||
use worktree::RepositoryEntry;
|
use worktree::RepositoryEntry;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
pub enum StatusAction {
|
|
||||||
Stage,
|
|
||||||
Unstage,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct GitState {
|
pub struct GitState {
|
||||||
/// The current commit message being composed.
|
/// The current commit message being composed.
|
||||||
pub commit_message: Option<SharedString>,
|
pub commit_message: SharedString,
|
||||||
|
|
||||||
/// When a git repository is selected, this is used to track which repository's changes
|
/// When a git repository is selected, this is used to track which repository's changes
|
||||||
/// are currently being viewed or modified in the UI.
|
/// are currently being viewed or modified in the UI.
|
||||||
pub active_repository: Option<(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)>,
|
pub active_repository: Option<(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)>,
|
||||||
|
|
||||||
pub update_sender: mpsc::UnboundedSender<(Arc<dyn GitRepository>, Vec<RepoPath>, StatusAction)>,
|
update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Message {
|
||||||
|
StageAndCommit(Arc<dyn GitRepository>, SharedString, Vec<RepoPath>),
|
||||||
|
Commit(Arc<dyn GitRepository>, SharedString),
|
||||||
|
Stage(Arc<dyn GitRepository>, Vec<RepoPath>),
|
||||||
|
Unstage(Arc<dyn GitRepository>, Vec<RepoPath>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GitState {
|
impl GitState {
|
||||||
pub fn new(cx: &AppContext) -> Self {
|
pub fn new(cx: &AppContext) -> Self {
|
||||||
let (tx, mut rx) =
|
let (update_sender, mut update_receiver) =
|
||||||
mpsc::unbounded::<(Arc<dyn GitRepository>, Vec<RepoPath>, StatusAction)>();
|
mpsc::unbounded::<(Message, mpsc::Sender<anyhow::Error>)>();
|
||||||
cx.spawn(|cx| async move {
|
cx.spawn(|cx| async move {
|
||||||
while let Some((git_repo, paths, action)) = rx.next().await {
|
while let Some((msg, mut err_sender)) = update_receiver.next().await {
|
||||||
cx.background_executor()
|
let result = cx
|
||||||
|
.background_executor()
|
||||||
.spawn(async move {
|
.spawn(async move {
|
||||||
match action {
|
match msg {
|
||||||
StatusAction::Stage => git_repo.stage_paths(&paths),
|
Message::StageAndCommit(repo, message, paths) => {
|
||||||
StatusAction::Unstage => git_repo.unstage_paths(&paths),
|
repo.stage_paths(&paths)?;
|
||||||
|
repo.commit(&message)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Message::Stage(repo, paths) => repo.stage_paths(&paths),
|
||||||
|
Message::Unstage(repo, paths) => repo.unstage_paths(&paths),
|
||||||
|
Message::Commit(repo, message) => repo.commit(&message),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await;
|
||||||
.log_err();
|
if let Err(e) = result {
|
||||||
|
err_sender.send(e).await.ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
GitState {
|
GitState {
|
||||||
commit_message: None,
|
commit_message: SharedString::default(),
|
||||||
active_repository: None,
|
active_repository: None,
|
||||||
update_sender: tx,
|
update_sender,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,55 +78,64 @@ impl GitState {
|
||||||
self.active_repository.as_ref()
|
self.active_repository.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn commit_message(&mut self, message: Option<SharedString>) {
|
pub fn stage_entries(
|
||||||
self.commit_message = message;
|
&self,
|
||||||
}
|
entries: Vec<RepoPath>,
|
||||||
|
err_sender: mpsc::Sender<anyhow::Error>,
|
||||||
pub fn clear_commit_message(&mut self) {
|
) -> anyhow::Result<()> {
|
||||||
self.commit_message = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn act_on_entries(&self, entries: Vec<RepoPath>, action: StatusAction) {
|
|
||||||
if entries.is_empty() {
|
if entries.is_empty() {
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
if let Some((_, _, git_repo)) = self.active_repository.as_ref() {
|
let Some((_, _, git_repo)) = self.active_repository.as_ref() else {
|
||||||
let _ = self
|
return Err(anyhow!("No active repository"));
|
||||||
.update_sender
|
};
|
||||||
.unbounded_send((git_repo.clone(), entries, action));
|
self.update_sender
|
||||||
|
.unbounded_send((Message::Stage(git_repo.clone(), entries), err_sender))
|
||||||
|
.map_err(|_| anyhow!("Failed to submit stage operation"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unstage_entries(
|
||||||
|
&self,
|
||||||
|
entries: Vec<RepoPath>,
|
||||||
|
err_sender: mpsc::Sender<anyhow::Error>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if entries.is_empty() {
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
let Some((_, _, git_repo)) = self.active_repository.as_ref() else {
|
||||||
|
return Err(anyhow!("No active repository"));
|
||||||
|
};
|
||||||
|
self.update_sender
|
||||||
|
.unbounded_send((Message::Unstage(git_repo.clone(), entries), err_sender))
|
||||||
|
.map_err(|_| anyhow!("Failed to submit unstage operation"))?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stage_entries(&self, entries: Vec<RepoPath>) {
|
pub fn stage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
|
||||||
self.act_on_entries(entries, StatusAction::Stage);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn unstage_entries(&self, entries: Vec<RepoPath>) {
|
|
||||||
self.act_on_entries(entries, StatusAction::Unstage);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stage_all(&self) {
|
|
||||||
let Some((_, entry, _)) = self.active_repository.as_ref() else {
|
let Some((_, entry, _)) = self.active_repository.as_ref() else {
|
||||||
return;
|
return Err(anyhow!("No active repository"));
|
||||||
};
|
};
|
||||||
let to_stage = entry
|
let to_stage = entry
|
||||||
.status()
|
.status()
|
||||||
.filter(|entry| !entry.status.is_staged().unwrap_or(false))
|
.filter(|entry| !entry.status.is_staged().unwrap_or(false))
|
||||||
.map(|entry| entry.repo_path.clone())
|
.map(|entry| entry.repo_path.clone())
|
||||||
.collect();
|
.collect();
|
||||||
self.stage_entries(to_stage);
|
self.stage_entries(to_stage, err_sender)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unstage_all(&self) {
|
pub fn unstage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
|
||||||
let Some((_, entry, _)) = self.active_repository.as_ref() else {
|
let Some((_, entry, _)) = self.active_repository.as_ref() else {
|
||||||
return;
|
return Err(anyhow!("No active repository"));
|
||||||
};
|
};
|
||||||
let to_unstage = entry
|
let to_unstage = entry
|
||||||
.status()
|
.status()
|
||||||
.filter(|entry| entry.status.is_staged().unwrap_or(true))
|
.filter(|entry| entry.status.is_staged().unwrap_or(true))
|
||||||
.map(|entry| entry.repo_path.clone())
|
.map(|entry| entry.repo_path.clone())
|
||||||
.collect();
|
.collect();
|
||||||
self.unstage_entries(to_unstage);
|
self.unstage_entries(to_unstage, err_sender)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a count of all entries in the active repository, including
|
/// Get a count of all entries in the active repository, including
|
||||||
|
@ -123,4 +145,61 @@ impl GitState {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(0, |(_, entry, _)| entry.status_len())
|
.map_or(0, |(_, entry, _)| entry.status_len())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn have_changes(&self) -> bool {
|
||||||
|
let Some((_, entry, _)) = self.active_repository.as_ref() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
entry.status_summary() != GitSummary::UNCHANGED
|
||||||
|
}
|
||||||
|
|
||||||
|
fn have_staged_changes(&self) -> bool {
|
||||||
|
let Some((_, entry, _)) = self.active_repository.as_ref() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
entry.status_summary().index != TrackedSummary::UNCHANGED
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_commit(&self, commit_all: bool) -> bool {
|
||||||
|
return !self.commit_message.trim().is_empty()
|
||||||
|
&& self.have_changes()
|
||||||
|
&& (commit_all || self.have_staged_changes());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn commit(&mut self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
|
||||||
|
if !self.can_commit(false) {
|
||||||
|
return Err(anyhow!("Unable to commit"));
|
||||||
|
}
|
||||||
|
let Some((_, _, git_repo)) = self.active_repository() else {
|
||||||
|
return Err(anyhow!("No active repository"));
|
||||||
|
};
|
||||||
|
let git_repo = git_repo.clone();
|
||||||
|
let message = std::mem::take(&mut self.commit_message);
|
||||||
|
self.update_sender
|
||||||
|
.unbounded_send((Message::Commit(git_repo, message), err_sender))
|
||||||
|
.map_err(|_| anyhow!("Failed to submit commit operation"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn commit_all(&mut self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
|
||||||
|
if !self.can_commit(true) {
|
||||||
|
return Err(anyhow!("Unable to commit"));
|
||||||
|
}
|
||||||
|
let Some((_, entry, git_repo)) = self.active_repository.as_ref() else {
|
||||||
|
return Err(anyhow!("No active repository"));
|
||||||
|
};
|
||||||
|
let to_stage = entry
|
||||||
|
.status()
|
||||||
|
.filter(|entry| !entry.status.is_staged().unwrap_or(false))
|
||||||
|
.map(|entry| entry.repo_path.clone())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let message = std::mem::take(&mut self.commit_message);
|
||||||
|
self.update_sender
|
||||||
|
.unbounded_send((
|
||||||
|
Message::StageAndCommit(git_repo.clone(), message, to_stage),
|
||||||
|
err_sender,
|
||||||
|
))
|
||||||
|
.map_err(|_| anyhow!("Failed to submit commit operation"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1588,7 +1588,7 @@ impl ProjectPanel {
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
&& entry.is_file()
|
&& entry.is_file()
|
||||||
&& entry.git_summary.modified > 0
|
&& entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
@ -1666,7 +1666,7 @@ impl ProjectPanel {
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
&& entry.is_file()
|
&& entry.is_file()
|
||||||
&& entry.git_summary.modified > 0
|
&& entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
|
@ -231,6 +231,10 @@ impl RepositoryEntry {
|
||||||
self.statuses_by_path.summary().item_summary.count
|
self.statuses_by_path.summary().item_summary.count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn status_summary(&self) -> GitSummary {
|
||||||
|
self.statuses_by_path.summary().item_summary
|
||||||
|
}
|
||||||
|
|
||||||
pub fn status_for_path(&self, path: &RepoPath) -> Option<StatusEntry> {
|
pub fn status_for_path(&self, path: &RepoPath) -> Option<StatusEntry> {
|
||||||
self.statuses_by_path
|
self.statuses_by_path
|
||||||
.get(&PathKey(path.0.clone()), &())
|
.get(&PathKey(path.0.clone()), &())
|
||||||
|
|
|
@ -6,7 +6,8 @@ use anyhow::Result;
|
||||||
use fs::{FakeFs, Fs, RealFs, RemoveOptions};
|
use fs::{FakeFs, Fs, RealFs, RemoveOptions};
|
||||||
use git::{
|
use git::{
|
||||||
status::{
|
status::{
|
||||||
FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,
|
FileStatus, GitSummary, StatusCode, TrackedStatus, TrackedSummary, UnmergedStatus,
|
||||||
|
UnmergedStatusCode,
|
||||||
},
|
},
|
||||||
GITIGNORE,
|
GITIGNORE,
|
||||||
};
|
};
|
||||||
|
@ -745,7 +746,7 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
|
||||||
Path::new("/root/tree/.git"),
|
Path::new("/root/tree/.git"),
|
||||||
&[(
|
&[(
|
||||||
Path::new("tracked-dir/tracked-file2"),
|
Path::new("tracked-dir/tracked-file2"),
|
||||||
FileStatus::worktree(StatusCode::Added),
|
StatusCode::Added.index(),
|
||||||
)],
|
)],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -830,7 +831,7 @@ async fn test_update_gitignore(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
fs.set_status_for_repo_via_working_copy_change(
|
fs.set_status_for_repo_via_working_copy_change(
|
||||||
Path::new("/root/.git"),
|
Path::new("/root/.git"),
|
||||||
&[(Path::new("b.txt"), FileStatus::worktree(StatusCode::Added))],
|
&[(Path::new("b.txt"), StatusCode::Added.index())],
|
||||||
);
|
);
|
||||||
|
|
||||||
cx.executor().run_until_parked();
|
cx.executor().run_until_parked();
|
||||||
|
@ -1500,10 +1501,7 @@ async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) {
|
||||||
// detected.
|
// detected.
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
Path::new("/root/.git"),
|
Path::new("/root/.git"),
|
||||||
&[(
|
&[(Path::new("b/c.txt"), StatusCode::Modified.index())],
|
||||||
Path::new("b/c.txt"),
|
|
||||||
FileStatus::worktree(StatusCode::Modified),
|
|
||||||
)],
|
|
||||||
);
|
);
|
||||||
cx.executor().run_until_parked();
|
cx.executor().run_until_parked();
|
||||||
|
|
||||||
|
@ -2199,7 +2197,7 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
||||||
assert_eq!(repo.path.as_ref(), Path::new("projects/project1"));
|
assert_eq!(repo.path.as_ref(), Path::new("projects/project1"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tree.status_for_file(Path::new("projects/project1/a")),
|
tree.status_for_file(Path::new("projects/project1/a")),
|
||||||
Some(FileStatus::worktree(StatusCode::Modified)),
|
Some(StatusCode::Modified.worktree()),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tree.status_for_file(Path::new("projects/project1/b")),
|
tree.status_for_file(Path::new("projects/project1/b")),
|
||||||
|
@ -2220,7 +2218,7 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
||||||
assert_eq!(repo.path.as_ref(), Path::new("projects/project2"));
|
assert_eq!(repo.path.as_ref(), Path::new("projects/project2"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tree.status_for_file(Path::new("projects/project2/a")),
|
tree.status_for_file(Path::new("projects/project2/a")),
|
||||||
Some(FileStatus::worktree(StatusCode::Modified)),
|
Some(StatusCode::Modified.worktree()),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tree.status_for_file(Path::new("projects/project2/b")),
|
tree.status_for_file(Path::new("projects/project2/b")),
|
||||||
|
@ -2421,7 +2419,7 @@ async fn test_file_status(cx: &mut TestAppContext) {
|
||||||
let snapshot = tree.snapshot();
|
let snapshot = tree.snapshot();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snapshot.status_for_file(project_path.join(A_TXT)),
|
snapshot.status_for_file(project_path.join(A_TXT)),
|
||||||
Some(FileStatus::worktree(StatusCode::Modified)),
|
Some(StatusCode::Modified.worktree()),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2463,7 +2461,7 @@ async fn test_file_status(cx: &mut TestAppContext) {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snapshot.status_for_file(project_path.join(E_TXT)),
|
snapshot.status_for_file(project_path.join(E_TXT)),
|
||||||
Some(FileStatus::worktree(StatusCode::Modified)),
|
Some(StatusCode::Modified.worktree()),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2575,14 +2573,11 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
assert_eq!(entries.len(), 3);
|
assert_eq!(entries.len(), 3);
|
||||||
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
||||||
assert_eq!(
|
assert_eq!(entries[0].status, StatusCode::Modified.worktree());
|
||||||
entries[0].status,
|
|
||||||
FileStatus::worktree(StatusCode::Modified)
|
|
||||||
);
|
|
||||||
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
|
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
|
||||||
assert_eq!(entries[1].status, FileStatus::Untracked);
|
assert_eq!(entries[1].status, FileStatus::Untracked);
|
||||||
assert_eq!(entries[2].repo_path.as_ref(), Path::new("d.txt"));
|
assert_eq!(entries[2].repo_path.as_ref(), Path::new("d.txt"));
|
||||||
assert_eq!(entries[2].status, FileStatus::worktree(StatusCode::Deleted));
|
assert_eq!(entries[2].status, StatusCode::Deleted.worktree());
|
||||||
});
|
});
|
||||||
|
|
||||||
std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
|
std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
|
||||||
|
@ -2600,20 +2595,14 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
std::assert_eq!(entries.len(), 4, "entries: {entries:?}");
|
std::assert_eq!(entries.len(), 4, "entries: {entries:?}");
|
||||||
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
||||||
assert_eq!(
|
assert_eq!(entries[0].status, StatusCode::Modified.worktree());
|
||||||
entries[0].status,
|
|
||||||
FileStatus::worktree(StatusCode::Modified)
|
|
||||||
);
|
|
||||||
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
|
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
|
||||||
assert_eq!(entries[1].status, FileStatus::Untracked);
|
assert_eq!(entries[1].status, FileStatus::Untracked);
|
||||||
// Status updated
|
// Status updated
|
||||||
assert_eq!(entries[2].repo_path.as_ref(), Path::new("c.txt"));
|
assert_eq!(entries[2].repo_path.as_ref(), Path::new("c.txt"));
|
||||||
assert_eq!(
|
assert_eq!(entries[2].status, StatusCode::Modified.worktree());
|
||||||
entries[2].status,
|
|
||||||
FileStatus::worktree(StatusCode::Modified)
|
|
||||||
);
|
|
||||||
assert_eq!(entries[3].repo_path.as_ref(), Path::new("d.txt"));
|
assert_eq!(entries[3].repo_path.as_ref(), Path::new("d.txt"));
|
||||||
assert_eq!(entries[3].status, FileStatus::worktree(StatusCode::Deleted));
|
assert_eq!(entries[3].status, StatusCode::Deleted.worktree());
|
||||||
});
|
});
|
||||||
|
|
||||||
git_add("a.txt", &repo);
|
git_add("a.txt", &repo);
|
||||||
|
@ -2646,7 +2635,7 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
|
||||||
&entries
|
&entries
|
||||||
);
|
);
|
||||||
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
||||||
assert_eq!(entries[0].status, FileStatus::worktree(StatusCode::Deleted));
|
assert_eq!(entries[0].status, StatusCode::Deleted.worktree());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2769,11 +2758,8 @@ async fn test_traverse_with_git_status(cx: &mut TestAppContext) {
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
Path::new("/root/x/.git"),
|
Path::new("/root/x/.git"),
|
||||||
&[
|
&[
|
||||||
(
|
(Path::new("x2.txt"), StatusCode::Modified.index()),
|
||||||
Path::new("x2.txt"),
|
(Path::new("z.txt"), StatusCode::Added.index()),
|
||||||
FileStatus::worktree(StatusCode::Modified),
|
|
||||||
),
|
|
||||||
(Path::new("z.txt"), FileStatus::worktree(StatusCode::Added)),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
|
@ -2782,7 +2768,7 @@ async fn test_traverse_with_git_status(cx: &mut TestAppContext) {
|
||||||
);
|
);
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
Path::new("/root/z/.git"),
|
Path::new("/root/z/.git"),
|
||||||
&[(Path::new("z2.txt"), FileStatus::worktree(StatusCode::Added))],
|
&[(Path::new("z2.txt"), StatusCode::Added.index())],
|
||||||
);
|
);
|
||||||
|
|
||||||
let tree = Worktree::local(
|
let tree = Worktree::local(
|
||||||
|
@ -2862,14 +2848,8 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
Path::new("/root/.git"),
|
Path::new("/root/.git"),
|
||||||
&[
|
&[
|
||||||
(
|
(Path::new("a/b/c1.txt"), StatusCode::Added.index()),
|
||||||
Path::new("a/b/c1.txt"),
|
(Path::new("a/d/e2.txt"), StatusCode::Modified.index()),
|
||||||
FileStatus::worktree(StatusCode::Added),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Path::new("a/d/e2.txt"),
|
|
||||||
FileStatus::worktree(StatusCode::Modified),
|
|
||||||
),
|
|
||||||
(Path::new("g/h2.txt"), CONFLICT),
|
(Path::new("g/h2.txt"), CONFLICT),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -2971,24 +2951,18 @@ async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext
|
||||||
|
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
Path::new("/root/x/.git"),
|
Path::new("/root/x/.git"),
|
||||||
&[(Path::new("x1.txt"), FileStatus::worktree(StatusCode::Added))],
|
&[(Path::new("x1.txt"), StatusCode::Added.index())],
|
||||||
);
|
);
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
Path::new("/root/y/.git"),
|
Path::new("/root/y/.git"),
|
||||||
&[
|
&[
|
||||||
(Path::new("y1.txt"), CONFLICT),
|
(Path::new("y1.txt"), CONFLICT),
|
||||||
(
|
(Path::new("y2.txt"), StatusCode::Modified.index()),
|
||||||
Path::new("y2.txt"),
|
|
||||||
FileStatus::worktree(StatusCode::Modified),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
Path::new("/root/z/.git"),
|
Path::new("/root/z/.git"),
|
||||||
&[(
|
&[(Path::new("z2.txt"), StatusCode::Modified.index())],
|
||||||
Path::new("z2.txt"),
|
|
||||||
FileStatus::worktree(StatusCode::Modified),
|
|
||||||
)],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let tree = Worktree::local(
|
let tree = Worktree::local(
|
||||||
|
@ -3081,11 +3055,8 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
Path::new("/root/x/.git"),
|
Path::new("/root/x/.git"),
|
||||||
&[
|
&[
|
||||||
(
|
(Path::new("x2.txt"), StatusCode::Modified.index()),
|
||||||
Path::new("x2.txt"),
|
(Path::new("z.txt"), StatusCode::Added.index()),
|
||||||
FileStatus::worktree(StatusCode::Modified),
|
|
||||||
),
|
|
||||||
(Path::new("z.txt"), FileStatus::worktree(StatusCode::Added)),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
|
@ -3095,7 +3066,7 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
Path::new("/root/z/.git"),
|
Path::new("/root/z/.git"),
|
||||||
&[(Path::new("z2.txt"), FileStatus::worktree(StatusCode::Added))],
|
&[(Path::new("z2.txt"), StatusCode::Added.index())],
|
||||||
);
|
);
|
||||||
|
|
||||||
let tree = Worktree::local(
|
let tree = Worktree::local(
|
||||||
|
@ -3227,12 +3198,12 @@ fn check_git_statuses(snapshot: &Snapshot, expected_statuses: &[(&Path, GitSumma
|
||||||
}
|
}
|
||||||
|
|
||||||
const ADDED: GitSummary = GitSummary {
|
const ADDED: GitSummary = GitSummary {
|
||||||
added: 1,
|
index: TrackedSummary::ADDED,
|
||||||
count: 1,
|
count: 1,
|
||||||
..GitSummary::UNCHANGED
|
..GitSummary::UNCHANGED
|
||||||
};
|
};
|
||||||
const MODIFIED: GitSummary = GitSummary {
|
const MODIFIED: GitSummary = GitSummary {
|
||||||
modified: 1,
|
index: TrackedSummary::MODIFIED,
|
||||||
count: 1,
|
count: 1,
|
||||||
..GitSummary::UNCHANGED
|
..GitSummary::UNCHANGED
|
||||||
};
|
};
|
||||||
|
@ -3378,15 +3349,15 @@ fn init_test(cx: &mut gpui::TestAppContext) {
|
||||||
fn assert_entry_git_state(
|
fn assert_entry_git_state(
|
||||||
tree: &Worktree,
|
tree: &Worktree,
|
||||||
path: &str,
|
path: &str,
|
||||||
worktree_status: Option<StatusCode>,
|
index_status: Option<StatusCode>,
|
||||||
is_ignored: bool,
|
is_ignored: bool,
|
||||||
) {
|
) {
|
||||||
let entry = tree.entry_for_path(path).expect("entry {path} not found");
|
let entry = tree.entry_for_path(path).expect("entry {path} not found");
|
||||||
let status = tree.status_for_file(Path::new(path));
|
let status = tree.status_for_file(Path::new(path));
|
||||||
let expected = worktree_status.map(|worktree_status| {
|
let expected = index_status.map(|index_status| {
|
||||||
TrackedStatus {
|
TrackedStatus {
|
||||||
worktree_status,
|
index_status,
|
||||||
index_status: StatusCode::Unmodified,
|
worktree_status: StatusCode::Unmodified,
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue