Rework shared commit editors (#24274)

Rework of https://github.com/zed-industries/zed/pull/24130
Uses
1033c0b57e
`COMMIT_EDITMSG` language-related definitions (thanks @d1y )

Instead of using real `.git/COMMIT_EDITMSG` file, create a buffer
without FS representation, stored in the `Repository` and shared the
regular way via the `BufferStore`.
Adds a knowledge of what `Git Commit` language is, and uses it in the
buffers which are rendered in the git panel.


Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad@zed.dev>
Co-authored-by: d1y <chenhonzhou@gmail.com>
Co-authored-by: Smit <smit@zed.dev>
This commit is contained in:
Kirill Bulatov 2025-02-05 17:36:24 +02:00 committed by GitHub
parent da4bad3a55
commit 868e3f75b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 428 additions and 372 deletions

View file

@ -4,7 +4,7 @@ use crate::ProjectDiff;
use crate::{
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
};
use anyhow::{Context as _, Result};
use anyhow::Result;
use collections::HashMap;
use db::kvp::KEY_VALUE_STORE;
use editor::actions::MoveToEnd;
@ -12,13 +12,12 @@ use editor::scroll::ScrollbarAutoHide;
use editor::{Editor, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar};
use git::repository::RepoPath;
use git::status::FileStatus;
use git::{CommitAllChanges, CommitChanges, ToggleStaged, COMMIT_MESSAGE};
use git::{CommitAllChanges, CommitChanges, ToggleStaged};
use gpui::*;
use language::{Buffer, BufferId};
use language::Buffer;
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
use project::git::{GitEvent, GitRepo, RepositoryHandle};
use project::{CreateOptions, Fs, Project, ProjectPath};
use rpc::proto;
use project::git::{GitEvent, Repository};
use project::{Fs, Project, ProjectPath};
use serde::{Deserialize, Serialize};
use settings::Settings as _;
use std::{collections::HashSet, path::PathBuf, sync::Arc, time::Duration, usize};
@ -32,7 +31,7 @@ use workspace::notifications::{DetachAndPromptErr, NotificationId};
use workspace::Toast;
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
Item, Workspace,
Workspace,
};
actions!(
@ -144,7 +143,7 @@ pub struct GitPanel {
pending_serialization: Task<Option<()>>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
active_repository: Option<RepositoryHandle>,
active_repository: Option<Entity<Repository>>,
scroll_handle: UniformListScrollHandle,
scrollbar_state: ScrollbarState,
selected_entry: Option<usize>,
@ -162,63 +161,6 @@ pub struct GitPanel {
can_commit_all: bool,
}
fn commit_message_buffer(
project: &Entity<Project>,
active_repository: &RepositoryHandle,
cx: &mut App,
) -> Task<Result<Entity<Buffer>>> {
match &active_repository.git_repo {
GitRepo::Local(repo) => {
let commit_message_file = repo.dot_git_dir().join(*COMMIT_MESSAGE);
let fs = project.read(cx).fs().clone();
let project = project.downgrade();
cx.spawn(|mut cx| async move {
fs.create_file(
&commit_message_file,
CreateOptions {
overwrite: false,
ignore_if_exists: true,
},
)
.await
.with_context(|| format!("creating commit message file {commit_message_file:?}"))?;
let buffer = project
.update(&mut cx, |project, cx| {
project.open_local_buffer(&commit_message_file, cx)
})?
.await
.with_context(|| {
format!("opening commit message buffer at {commit_message_file:?}",)
})?;
Ok(buffer)
})
}
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => {
let request = client.request(proto::OpenCommitMessageBuffer {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
});
let project = project.downgrade();
cx.spawn(|mut cx| async move {
let response = request.await.context("requesting to open commit buffer")?;
let buffer_id = BufferId::new(response.buffer_id)?;
let buffer = project
.update(&mut cx, {
|project, cx| project.wait_for_remote_buffer(buffer_id, cx)
})?
.await?;
Ok(buffer)
})
}
}
}
fn commit_message_editor(
commit_message_buffer: Option<Entity<Buffer>>,
window: &mut Window,
@ -360,7 +302,7 @@ impl GitPanel {
let Some(git_repo) = self.active_repository.as_ref() else {
return;
};
let Some(repo_path) = git_repo.project_path_to_repo_path(&path) else {
let Some(repo_path) = git_repo.read(cx).project_path_to_repo_path(&path) else {
return;
};
let Some(ix) = self.entries_by_path.get(&repo_path) else {
@ -578,7 +520,7 @@ impl GitPanel {
.active_repository
.as_ref()
.map_or(false, |active_repository| {
active_repository.entry_count() > 0
active_repository.read(cx).entry_count() > 0
});
if have_entries && self.selected_entry.is_none() {
self.selected_entry = Some(0);
@ -655,11 +597,17 @@ impl GitPanel {
let repo_paths = repo_paths.clone();
let active_repository = active_repository.clone();
|this, mut cx| async move {
let result = if stage {
active_repository.stage_entries(repo_paths.clone()).await
} else {
active_repository.unstage_entries(repo_paths.clone()).await
};
let result = cx
.update(|cx| {
if stage {
active_repository.read(cx).stage_entries(repo_paths.clone())
} else {
active_repository
.read(cx)
.unstage_entries(repo_paths.clone())
}
})?
.await?;
this.update(&mut cx, |this, cx| {
for pending in this.pending.iter_mut() {
@ -697,7 +645,9 @@ impl GitPanel {
let Some(active_repository) = self.active_repository.as_ref() else {
return;
};
let Some(path) = active_repository.repo_path_to_project_path(&status_entry.repo_path)
let Some(path) = active_repository
.read(cx)
.repo_path_to_project_path(&status_entry.repo_path)
else {
return;
};
@ -725,18 +675,18 @@ impl GitPanel {
if !self.can_commit {
return;
}
if self.commit_editor.read(cx).is_empty(cx) {
let message = self.commit_editor.read(cx).text(cx);
if message.trim().is_empty() {
return;
}
self.commit_pending = true;
let save_task = self.commit_editor.update(cx, |editor, cx| {
editor.save(false, self.project.clone(), window, cx)
});
let commit_editor = self.commit_editor.clone();
self.commit_task = cx.spawn_in(window, |git_panel, mut cx| async move {
let commit = active_repository.update(&mut cx, |active_repository, _| {
active_repository.commit(SharedString::from(message), name_and_email)
})?;
let result = maybe!(async {
save_task.await?;
active_repository.commit(name_and_email).await?;
commit.await??;
cx.update(|window, cx| {
commit_editor.update(cx, |editor, cx| editor.clear(window, cx));
})
@ -768,14 +718,12 @@ impl GitPanel {
if !self.can_commit_all {
return;
}
if self.commit_editor.read(cx).is_empty(cx) {
let message = self.commit_editor.read(cx).text(cx);
if message.trim().is_empty() {
return;
}
self.commit_pending = true;
let save_task = self.commit_editor.update(cx, |editor, cx| {
editor.save(false, self.project.clone(), window, cx)
});
let commit_editor = self.commit_editor.clone();
let tracked_files = self
.entries
@ -790,9 +738,15 @@ impl GitPanel {
self.commit_task = cx.spawn_in(window, |git_panel, mut cx| async move {
let result = maybe!(async {
save_task.await?;
active_repository.stage_entries(tracked_files).await?;
active_repository.commit(name_and_email).await
cx.update(|_, cx| active_repository.read(cx).stage_entries(tracked_files))?
.await??;
cx.update(|_, cx| {
active_repository
.read(cx)
.commit(SharedString::from(message), name_and_email)
})?
.await??;
Ok(())
})
.await;
cx.update(|window, cx| match result {
@ -886,47 +840,56 @@ impl GitPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
let project = self.project.clone();
let handle = cx.entity().downgrade();
self.reopen_commit_buffer(window, cx);
self.update_visible_entries_task = cx.spawn_in(window, |_, mut cx| async move {
cx.background_executor().timer(UPDATE_DEBOUNCE).await;
if let Some(git_panel) = handle.upgrade() {
let Ok(commit_message_buffer) = git_panel.update_in(&mut cx, |git_panel, _, cx| {
git_panel
.active_repository
.as_ref()
.map(|active_repository| {
commit_message_buffer(&project, active_repository, cx)
})
}) else {
return;
};
let commit_message_buffer = match commit_message_buffer {
Some(commit_message_buffer) => match commit_message_buffer
.await
.context("opening commit buffer on repo update")
.log_err()
{
Some(buffer) => Some(buffer),
None => return,
},
None => None,
};
git_panel
.update_in(&mut cx, |git_panel, window, cx| {
git_panel.update_visible_entries(cx);
.update_in(&mut cx, |git_panel, _, cx| {
if clear_pending {
git_panel.clear_pending();
}
git_panel.commit_editor =
cx.new(|cx| commit_message_editor(commit_message_buffer, window, cx));
git_panel.update_visible_entries(cx);
})
.ok();
}
});
}
fn reopen_commit_buffer(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(active_repo) = self.active_repository.as_ref() else {
return;
};
let load_buffer = active_repo.update(cx, |active_repo, cx| {
let project = self.project.read(cx);
active_repo.open_commit_buffer(
Some(project.languages().clone()),
project.buffer_store().clone(),
cx,
)
});
cx.spawn_in(window, |git_panel, mut cx| async move {
let buffer = load_buffer.await?;
git_panel.update_in(&mut cx, |git_panel, window, cx| {
if git_panel
.commit_editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.as_ref()
!= Some(&buffer)
{
git_panel.commit_editor =
cx.new(|cx| commit_message_editor(Some(buffer), window, cx));
}
})
})
.detach_and_log_err(cx);
}
fn clear_pending(&mut self) {
self.pending.retain(|v| !v.finished)
}
@ -944,6 +907,7 @@ impl GitPanel {
};
// First pass - collect all paths
let repo = repo.read(cx);
let path_set = HashSet::from_iter(repo.status().map(|entry| entry.repo_path));
let mut has_changed_checked_boxes = false;
@ -1117,7 +1081,7 @@ impl GitPanel {
let entry_count = self
.active_repository
.as_ref()
.map_or(0, RepositoryHandle::entry_count);
.map_or(0, |repo| repo.read(cx).entry_count());
let changes_string = match entry_count {
0 => "No changes".to_string(),
@ -1151,7 +1115,7 @@ impl GitPanel {
let active_repository = self.project.read(cx).active_repository(cx);
let repository_display_name = active_repository
.as_ref()
.map(|repo| repo.display_name(self.project.read(cx), cx))
.map(|repo| repo.read(cx).display_name(self.project.read(cx), cx))
.unwrap_or_default();
let entry_count = self.entries.len();
@ -1619,7 +1583,7 @@ impl Render for GitPanel {
.active_repository
.as_ref()
.map_or(false, |active_repository| {
active_repository.entry_count() > 0
active_repository.read(cx).entry_count() > 0
});
let room = self
.workspace

View file

@ -163,6 +163,7 @@ impl ProjectDiff {
};
let Some(path) = git_repo
.read(cx)
.repo_path_to_project_path(&entry.repo_path)
.and_then(|project_path| self.project.read(cx).absolute_path(&project_path, cx))
else {
@ -234,43 +235,45 @@ impl ProjectDiff {
let mut previous_paths = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
let mut result = vec![];
for entry in repo.status() {
if !entry.status.has_changes() {
continue;
repo.update(cx, |repo, cx| {
for entry in repo.status() {
if !entry.status.has_changes() {
continue;
}
let Some(project_path) = repo.repo_path_to_project_path(&entry.repo_path) else {
continue;
};
let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else {
continue;
};
// Craft some artificial paths so that created entries will appear last.
let path_key = if entry.status.is_created() {
PathKey::namespaced(ADDED_NAMESPACE, &abs_path)
} else {
PathKey::namespaced(CHANGED_NAMESPACE, &abs_path)
};
previous_paths.remove(&path_key);
let load_buffer = self
.project
.update(cx, |project, cx| project.open_buffer(project_path, cx));
let project = self.project.clone();
result.push(cx.spawn(|_, mut cx| async move {
let buffer = load_buffer.await?;
let changes = project
.update(&mut cx, |project, cx| {
project.open_uncommitted_changes(buffer.clone(), cx)
})?
.await?;
Ok(DiffBuffer {
path_key,
buffer,
change_set: changes,
})
}));
}
let Some(project_path) = repo.repo_path_to_project_path(&entry.repo_path) else {
continue;
};
let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else {
continue;
};
// Craft some artificial paths so that created entries will appear last.
let path_key = if entry.status.is_created() {
PathKey::namespaced(ADDED_NAMESPACE, &abs_path)
} else {
PathKey::namespaced(CHANGED_NAMESPACE, &abs_path)
};
previous_paths.remove(&path_key);
let load_buffer = self
.project
.update(cx, |project, cx| project.open_buffer(project_path, cx));
let project = self.project.clone();
result.push(cx.spawn(|_, mut cx| async move {
let buffer = load_buffer.await?;
let changes = project
.update(&mut cx, |project, cx| {
project.open_uncommitted_changes(buffer.clone(), cx)
})?
.await?;
Ok(DiffBuffer {
path_key,
buffer,
change_set: changes,
})
}));
}
});
self.multibuffer.update(cx, |multibuffer, cx| {
for path in previous_paths {
multibuffer.remove_excerpts_for_path(path, cx);

View file

@ -4,7 +4,7 @@ use gpui::{
};
use picker::{Picker, PickerDelegate};
use project::{
git::{GitState, RepositoryHandle},
git::{GitState, Repository},
Project,
};
use std::sync::Arc;
@ -117,13 +117,13 @@ impl<T: PopoverTrigger> RenderOnce for RepositorySelectorPopoverMenu<T> {
pub struct RepositorySelectorDelegate {
project: WeakEntity<Project>,
repository_selector: WeakEntity<RepositorySelector>,
repository_entries: Vec<RepositoryHandle>,
filtered_repositories: Vec<RepositoryHandle>,
repository_entries: Vec<Entity<Repository>>,
filtered_repositories: Vec<Entity<Repository>>,
selected_index: usize,
}
impl RepositorySelectorDelegate {
pub fn update_repository_entries(&mut self, all_repositories: Vec<RepositoryHandle>) {
pub fn update_repository_entries(&mut self, all_repositories: Vec<Entity<Repository>>) {
self.repository_entries = all_repositories.clone();
self.filtered_repositories = all_repositories;
self.selected_index = 0;
@ -194,7 +194,7 @@ impl PickerDelegate for RepositorySelectorDelegate {
let Some(selected_repo) = self.filtered_repositories.get(self.selected_index) else {
return;
};
selected_repo.activate(cx);
selected_repo.update(cx, |selected_repo, cx| selected_repo.activate(cx));
self.dismissed(window, cx);
}
@ -222,7 +222,7 @@ impl PickerDelegate for RepositorySelectorDelegate {
) -> Option<Self::ListItem> {
let project = self.project.upgrade()?;
let repo_info = self.filtered_repositories.get(ix)?;
let display_name = repo_info.display_name(project.read(cx), cx);
let display_name = repo_info.read(cx).display_name(project.read(cx), cx);
// TODO: Implement repository item rendering
Some(
ListItem::new(ix)