Detailed changes
@@ -5254,7 +5254,6 @@ dependencies = [
"picker",
"postage",
"project",
- "rpc",
"schemars",
"serde",
"serde_derive",
@@ -7008,6 +7007,7 @@ dependencies = [
"tree-sitter-cpp",
"tree-sitter-css",
"tree-sitter-diff",
+ "tree-sitter-gitcommit",
"tree-sitter-go",
"tree-sitter-gomod",
"tree-sitter-gowork",
@@ -13944,6 +13944,15 @@ dependencies = [
"tree-sitter-language",
]
+[[package]]
+name = "tree-sitter-gitcommit"
+version = "0.0.1"
+source = "git+https://github.com/zed-industries/tree-sitter-git-commit?rev=88309716a69dd13ab83443721ba6e0b491d37ee9#88309716a69dd13ab83443721ba6e0b491d37ee9"
+dependencies = [
+ "cc",
+ "tree-sitter-language",
+]
+
[[package]]
name = "tree-sitter-go"
version = "0.23.4"
@@ -522,6 +522,7 @@ tree-sitter-cpp = "0.23"
tree-sitter-css = "0.23"
tree-sitter-elixir = "0.3"
tree-sitter-embedded-template = "0.23.0"
+tree-sitter-gitcommit = {git = "https://github.com/zed-industries/tree-sitter-git-commit", rev = "88309716a69dd13ab83443721ba6e0b491d37ee9"}
tree-sitter-go = "0.23"
tree-sitter-go-mod = { git = "https://github.com/camdencheek/tree-sitter-go-mod", rev = "6efb59652d30e0e9cd5f3b3a669afd6f1a926d3c", package = "tree-sitter-gomod" }
tree-sitter-gowork = { git = "https://github.com/zed-industries/tree-sitter-go-work", rev = "acb0617bf7f4fda02c6217676cc64acb89536dc7" }
@@ -1,6 +1,6 @@
use crate::status::FileStatus;
+use crate::GitHostingProviderRegistry;
use crate::{blame::Blame, status::GitStatus};
-use crate::{GitHostingProviderRegistry, COMMIT_MESSAGE};
use anyhow::{anyhow, Context as _, Result};
use collections::{HashMap, HashSet};
use git2::BranchType;
@@ -68,7 +68,7 @@ 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.
fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()>;
- fn commit(&self, name_and_email: Option<(&str, &str)>) -> Result<()>;
+ fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()>;
}
impl std::fmt::Debug for dyn GitRepository {
@@ -298,22 +298,14 @@ impl GitRepository for RealGitRepository {
Ok(())
}
- fn commit(&self, name_and_email: Option<(&str, &str)>) -> Result<()> {
+ fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()> {
let working_directory = self
.repository
.lock()
.workdir()
.context("failed to read git work directory")?
.to_path_buf();
- let commit_file = self.dot_git_dir().join(*COMMIT_MESSAGE);
- let commit_file_path = commit_file.to_string_lossy();
- let mut args = vec![
- "commit",
- "--quiet",
- "-F",
- commit_file_path.as_ref(),
- "--cleanup=strip",
- ];
+ let mut args = vec!["commit", "--quiet", "-m", message, "--cleanup=strip"];
let author = name_and_email.map(|(name, email)| format!("{name} <{email}>"));
if let Some(author) = author.as_deref() {
args.push("--author");
@@ -480,7 +472,7 @@ impl GitRepository for FakeGitRepository {
unimplemented!()
}
- fn commit(&self, _name_and_email: Option<(&str, &str)>) -> Result<()> {
+ fn commit(&self, _message: &str, _name_and_email: Option<(&str, &str)>) -> Result<()> {
unimplemented!()
}
}
@@ -26,7 +26,6 @@ multi_buffer.workspace = true
menu.workspace = true
postage.workspace = true
project.workspace = true
-rpc.workspace = true
schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
@@ -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
@@ -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);
@@ -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)
@@ -19,6 +19,7 @@ load-grammars = [
"tree-sitter-cpp",
"tree-sitter-css",
"tree-sitter-diff",
+ "tree-sitter-gitcommit",
"tree-sitter-go",
"tree-sitter-go-mod",
"tree-sitter-gowork",
@@ -69,6 +70,7 @@ tree-sitter-c = { workspace = true, optional = true }
tree-sitter-cpp = { workspace = true, optional = true }
tree-sitter-css = { workspace = true, optional = true }
tree-sitter-diff = { workspace = true, optional = true }
+tree-sitter-gitcommit = {workspace = true, optional = true }
tree-sitter-go = { workspace = true, optional = true }
tree-sitter-go-mod = { workspace = true, optional = true }
tree-sitter-gowork = { workspace = true, optional = true }
@@ -0,0 +1,18 @@
+name = "Git Commit"
+grammar = "git_commit"
+path_suffixes = [
+ "TAG_EDITMSG",
+ "MERGE_MSG",
+ "COMMIT_EDITMSG",
+ "NOTES_EDITMSG",
+ "EDIT_DESCRIPTION",
+]
+line_comments = ["#"]
+brackets = [
+ { start = "(", end = ")", close = true, newline = false },
+ { start = "`", end = "`", close = true, newline = false },
+ { start = "\"", end = "\"", close = true, newline = false },
+ { start = "'", end = "'", close = true, newline = false },
+ { start = "{", end = "}", close = true, newline = false },
+ { start = "[", end = "]", close = true, newline = false },
+]
@@ -0,0 +1,18 @@
+(subject) @markup.heading
+(path) @string.special.path
+(branch) @string.special.symbol
+(commit) @constant
+(item) @markup.link.url
+(header) @tag
+
+(change kind: "new file" @diff.plus)
+(change kind: "deleted" @diff.minus)
+(change kind: "modified" @diff.delta)
+(change kind: "renamed" @diff.delta.moved)
+
+(trailer
+ key: (trailer_key) @variable.other.member
+ value: (trailer_value) @string)
+
+[":" "=" "->" (scissors)] @punctuation.delimiter
+(comment) @comment
@@ -0,0 +1,5 @@
+((scissors) @content
+ (#set! "language" "diff"))
+
+((rebase_command) @content
+ (#set! "language" "git_rebase"))
@@ -31,6 +31,25 @@ mod yaml;
#[exclude = "*.rs"]
struct LanguageDir;
+/// A shared grammar for plain text, exposed for reuse by downstream crates.
+#[cfg(feature = "tree-sitter-gitcommit")]
+pub static LANGUAGE_GIT_COMMIT: std::sync::LazyLock<Arc<Language>> =
+ std::sync::LazyLock::new(|| {
+ Arc::new(Language::new(
+ LanguageConfig {
+ name: "Git Commit".into(),
+ soft_wrap: Some(language::language_settings::SoftWrap::EditorWidth),
+ matcher: LanguageMatcher {
+ path_suffixes: vec!["COMMIT_EDITMSG".to_owned()],
+ first_line_pattern: None,
+ },
+ line_comments: vec![Arc::from("#")],
+ ..LanguageConfig::default()
+ },
+ Some(tree_sitter_gitcommit::LANGUAGE.into()),
+ ))
+ });
+
pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mut App) {
#[cfg(feature = "load-grammars")]
languages.register_native_grammars([
@@ -53,6 +72,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mu
("tsx", tree_sitter_typescript::LANGUAGE_TSX),
("typescript", tree_sitter_typescript::LANGUAGE_TYPESCRIPT),
("yaml", tree_sitter_yaml::LANGUAGE),
+ ("gitcommit", tree_sitter_gitcommit::LANGUAGE),
]);
macro_rules! language {
@@ -1,6 +1,7 @@
+use crate::buffer_store::BufferStore;
use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
use crate::{Project, ProjectPath};
-use anyhow::{anyhow, Context as _};
+use anyhow::Context as _;
use client::ProjectId;
use futures::channel::{mpsc, oneshot};
use futures::StreamExt as _;
@@ -8,24 +9,28 @@ use git::{
repository::{GitRepository, RepoPath},
status::{GitSummary, TrackedSummary},
};
-use gpui::{App, Context, Entity, EventEmitter, SharedString, Subscription, WeakEntity};
+use gpui::{
+ App, AppContext, Context, Entity, EventEmitter, SharedString, Subscription, Task, WeakEntity,
+};
+use language::{Buffer, LanguageRegistry};
use rpc::{proto, AnyProtoClient};
use settings::WorktreeId;
use std::sync::Arc;
+use text::BufferId;
use util::{maybe, ResultExt};
use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry};
pub struct GitState {
project_id: Option<ProjectId>,
client: Option<AnyProtoClient>,
- repositories: Vec<RepositoryHandle>,
+ repositories: Vec<Entity<Repository>>,
active_index: Option<usize>,
update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<anyhow::Result<()>>)>,
_subscription: Subscription,
}
-#[derive(Clone)]
-pub struct RepositoryHandle {
+pub struct Repository {
+ commit_message_buffer: Option<Entity<Buffer>>,
git_state: WeakEntity<GitState>,
pub worktree_id: WorktreeId,
pub repository_entry: RepositoryEntry,
@@ -44,25 +49,10 @@ pub enum GitRepo {
},
}
-impl PartialEq<Self> for RepositoryHandle {
- fn eq(&self, other: &Self) -> bool {
- self.worktree_id == other.worktree_id
- && self.repository_entry.work_directory_id()
- == other.repository_entry.work_directory_id()
- }
-}
-
-impl Eq for RepositoryHandle {}
-
-impl PartialEq<RepositoryEntry> for RepositoryHandle {
- fn eq(&self, other: &RepositoryEntry) -> bool {
- self.repository_entry.work_directory_id() == other.work_directory_id()
- }
-}
-
enum Message {
Commit {
git_repo: GitRepo,
+ message: SharedString,
name_and_email: Option<(SharedString, SharedString)>,
},
Stage(GitRepo, Vec<RepoPath>),
@@ -97,7 +87,7 @@ impl GitState {
}
}
- pub fn active_repository(&self) -> Option<RepositoryHandle> {
+ pub fn active_repository(&self) -> Option<Entity<Repository>> {
self.active_index
.map(|index| self.repositories[index].clone())
}
@@ -118,7 +108,7 @@ impl GitState {
worktree_store.update(cx, |worktree_store, cx| {
for worktree in worktree_store.worktrees() {
- worktree.update(cx, |worktree, _| {
+ worktree.update(cx, |worktree, cx| {
let snapshot = worktree.snapshot();
for repo in snapshot.repositories().iter() {
let git_repo = worktree
@@ -139,27 +129,34 @@ impl GitState {
let Some(git_repo) = git_repo else {
continue;
};
- let existing = self
- .repositories
- .iter()
- .enumerate()
- .find(|(_, existing_handle)| existing_handle == &repo);
+ let worktree_id = worktree.id();
+ let existing =
+ self.repositories
+ .iter()
+ .enumerate()
+ .find(|(_, existing_handle)| {
+ existing_handle.read(cx).id()
+ == (worktree_id, repo.work_directory_id())
+ });
let handle = if let Some((index, handle)) = existing {
if self.active_index == Some(index) {
new_active_index = Some(new_repositories.len());
}
// Update the statuses but keep everything else.
- let mut existing_handle = handle.clone();
- existing_handle.repository_entry = repo.clone();
+ let existing_handle = handle.clone();
+ existing_handle.update(cx, |existing_handle, _| {
+ existing_handle.repository_entry = repo.clone();
+ });
existing_handle
} else {
- RepositoryHandle {
+ cx.new(|_| Repository {
git_state: this.clone(),
- worktree_id: worktree.id(),
+ worktree_id,
repository_entry: repo.clone(),
git_repo,
update_sender: self.update_sender.clone(),
- }
+ commit_message_buffer: None,
+ })
};
new_repositories.push(handle);
}
@@ -184,7 +181,7 @@ impl GitState {
}
}
- pub fn all_repositories(&self) -> Vec<RepositoryHandle> {
+ pub fn all_repositories(&self) -> Vec<Entity<Repository>> {
self.repositories.clone()
}
@@ -260,10 +257,12 @@ impl GitState {
}
Message::Commit {
git_repo,
+ message,
name_and_email,
} => {
match git_repo {
GitRepo::Local(repo) => repo.commit(
+ message.as_ref(),
name_and_email
.as_ref()
.map(|(name, email)| (name.as_ref(), email.as_ref())),
@@ -280,6 +279,7 @@ impl GitState {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
+ message: String::from(message),
name: name.map(String::from),
email: email.map(String::from),
})
@@ -293,7 +293,11 @@ impl GitState {
}
}
-impl RepositoryHandle {
+impl Repository {
+ fn id(&self) -> (WorktreeId, ProjectEntryId) {
+ (self.worktree_id, self.repository_entry.work_directory_id())
+ }
+
pub fn display_name(&self, project: &Project, cx: &App) -> SharedString {
maybe!({
let path = self.repo_path_to_project_path(&"".into())?;
@@ -318,7 +322,7 @@ impl RepositoryHandle {
.repositories
.iter()
.enumerate()
- .find(|(_, handle)| handle == &self)
+ .find(|(_, handle)| handle.read(cx).id() == self.id())
else {
return;
};
@@ -343,47 +347,121 @@ impl RepositoryHandle {
self.repository_entry.relativize(&path.path).log_err()
}
- pub async fn stage_entries(&self, entries: Vec<RepoPath>) -> anyhow::Result<()> {
- if entries.is_empty() {
- return Ok(());
+ pub fn open_commit_buffer(
+ &mut self,
+ languages: Option<Arc<LanguageRegistry>>,
+ buffer_store: Entity<BufferStore>,
+ cx: &mut Context<Self>,
+ ) -> Task<anyhow::Result<Entity<Buffer>>> {
+ if let Some(buffer) = self.commit_message_buffer.clone() {
+ return Task::ready(Ok(buffer));
}
+
+ if let GitRepo::Remote {
+ project_id,
+ client,
+ worktree_id,
+ work_directory_id,
+ } = self.git_repo.clone()
+ {
+ let client = client.clone();
+ cx.spawn(|repository, mut cx| async move {
+ 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 response = request.await.context("requesting to open commit buffer")?;
+ let buffer_id = BufferId::new(response.buffer_id)?;
+ let buffer = buffer_store
+ .update(&mut cx, |buffer_store, cx| {
+ buffer_store.wait_for_remote_buffer(buffer_id, cx)
+ })?
+ .await?;
+ if let Some(language_registry) = languages {
+ let git_commit_language =
+ language_registry.language_for_name("Git Commit").await?;
+ buffer.update(&mut cx, |buffer, cx| {
+ buffer.set_language(Some(git_commit_language), cx);
+ })?;
+ }
+ repository.update(&mut cx, |repository, _| {
+ repository.commit_message_buffer = Some(buffer.clone());
+ })?;
+ Ok(buffer)
+ })
+ } else {
+ self.open_local_commit_buffer(languages, buffer_store, cx)
+ }
+ }
+
+ fn open_local_commit_buffer(
+ &mut self,
+ language_registry: Option<Arc<LanguageRegistry>>,
+ buffer_store: Entity<BufferStore>,
+ cx: &mut Context<Self>,
+ ) -> Task<anyhow::Result<Entity<Buffer>>> {
+ cx.spawn(|repository, mut cx| async move {
+ let buffer = buffer_store
+ .update(&mut cx, |buffer_store, cx| buffer_store.create_buffer(cx))?
+ .await?;
+
+ if let Some(language_registry) = language_registry {
+ let git_commit_language = language_registry.language_for_name("Git Commit").await?;
+ buffer.update(&mut cx, |buffer, cx| {
+ buffer.set_language(Some(git_commit_language), cx);
+ })?;
+ }
+
+ repository.update(&mut cx, |repository, _| {
+ repository.commit_message_buffer = Some(buffer.clone());
+ })?;
+ Ok(buffer)
+ })
+ }
+
+ pub fn stage_entries(&self, entries: Vec<RepoPath>) -> oneshot::Receiver<anyhow::Result<()>> {
let (result_tx, result_rx) = futures::channel::oneshot::channel();
+ if entries.is_empty() {
+ result_tx.send(Ok(())).ok();
+ return result_rx;
+ }
self.update_sender
.unbounded_send((Message::Stage(self.git_repo.clone(), entries), result_tx))
- .map_err(|_| anyhow!("Failed to submit stage operation"))?;
-
- result_rx.await?
+ .ok();
+ result_rx
}
- pub async fn unstage_entries(&self, entries: Vec<RepoPath>) -> anyhow::Result<()> {
+ pub fn unstage_entries(&self, entries: Vec<RepoPath>) -> oneshot::Receiver<anyhow::Result<()>> {
+ let (result_tx, result_rx) = futures::channel::oneshot::channel();
if entries.is_empty() {
- return Ok(());
+ result_tx.send(Ok(())).ok();
+ return result_rx;
}
- let (result_tx, result_rx) = futures::channel::oneshot::channel();
self.update_sender
.unbounded_send((Message::Unstage(self.git_repo.clone(), entries), result_tx))
- .map_err(|_| anyhow!("Failed to submit unstage operation"))?;
- result_rx.await?
+ .ok();
+ result_rx
}
- pub async fn stage_all(&self) -> anyhow::Result<()> {
+ pub fn stage_all(&self) -> oneshot::Receiver<anyhow::Result<()>> {
let to_stage = self
.repository_entry
.status()
.filter(|entry| !entry.status.is_staged().unwrap_or(false))
.map(|entry| entry.repo_path.clone())
.collect();
- self.stage_entries(to_stage).await
+ self.stage_entries(to_stage)
}
- pub async fn unstage_all(&self) -> anyhow::Result<()> {
+ pub fn unstage_all(&self) -> oneshot::Receiver<anyhow::Result<()>> {
let to_unstage = self
.repository_entry
.status()
.filter(|entry| entry.status.is_staged().unwrap_or(true))
.map(|entry| entry.repo_path.clone())
.collect();
- self.unstage_entries(to_unstage).await
+ self.unstage_entries(to_unstage)
}
/// Get a count of all entries in the active repository, including
@@ -404,18 +482,22 @@ impl RepositoryHandle {
return self.have_changes() && (commit_all || self.have_staged_changes());
}
- pub async fn commit(
+ pub fn commit(
&self,
+ message: SharedString,
name_and_email: Option<(SharedString, SharedString)>,
- ) -> anyhow::Result<()> {
+ ) -> oneshot::Receiver<anyhow::Result<()>> {
let (result_tx, result_rx) = futures::channel::oneshot::channel();
- self.update_sender.unbounded_send((
- Message::Commit {
- git_repo: self.git_repo.clone(),
- name_and_email,
- },
- result_tx,
- ))?;
- result_rx.await?
+ self.update_sender
+ .unbounded_send((
+ Message::Commit {
+ git_repo: self.git_repo.clone(),
+ message,
+ name_and_email,
+ },
+ result_tx,
+ ))
+ .ok();
+ result_rx
}
}
@@ -22,7 +22,7 @@ mod project_tests;
mod direnv;
mod environment;
pub use environment::EnvironmentErrorMessage;
-use git::RepositoryHandle;
+use git::Repository;
pub mod search_history;
mod yarn;
@@ -48,7 +48,6 @@ use ::git::{
blame::Blame,
repository::{Branch, GitRepository, RepoPath},
status::FileStatus,
- COMMIT_MESSAGE,
};
use gpui::{
AnyEntity, App, AppContext as _, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter,
@@ -1998,12 +1997,15 @@ impl Project {
project_id,
id: id.into(),
});
- cx.spawn(move |this, mut cx| async move {
+ cx.spawn(move |project, mut cx| async move {
let buffer_id = BufferId::new(request.await?.buffer_id)?;
- this.update(&mut cx, |this, cx| {
- this.wait_for_remote_buffer(buffer_id, cx)
- })?
- .await
+ project
+ .update(&mut cx, |project, cx| {
+ project.buffer_store.update(cx, |buffer_store, cx| {
+ buffer_store.wait_for_remote_buffer(buffer_id, cx)
+ })
+ })?
+ .await
})
} else {
Task::ready(Err(anyhow!("cannot open buffer while disconnected")))
@@ -2846,16 +2848,21 @@ impl Project {
let proto_client = ssh_client.read(cx).proto_client();
- cx.spawn(|this, mut cx| async move {
+ cx.spawn(|project, mut cx| async move {
let buffer = proto_client
.request(proto::OpenServerSettings {
project_id: SSH_PROJECT_ID,
})
.await?;
- let buffer = this
- .update(&mut cx, |this, cx| {
- anyhow::Ok(this.wait_for_remote_buffer(BufferId::new(buffer.buffer_id)?, cx))
+ let buffer = project
+ .update(&mut cx, |project, cx| {
+ project.buffer_store.update(cx, |buffer_store, cx| {
+ anyhow::Ok(
+ buffer_store
+ .wait_for_remote_buffer(BufferId::new(buffer.buffer_id)?, cx),
+ )
+ })
})??
.await;
@@ -3186,13 +3193,15 @@ impl Project {
});
let guard = self.retain_remotely_created_models(cx);
- cx.spawn(move |this, mut cx| async move {
+ cx.spawn(move |project, mut cx| async move {
let response = request.await?;
for buffer_id in response.buffer_ids {
let buffer_id = BufferId::new(buffer_id)?;
- let buffer = this
- .update(&mut cx, |this, cx| {
- this.wait_for_remote_buffer(buffer_id, cx)
+ let buffer = project
+ .update(&mut cx, |project, cx| {
+ project.buffer_store.update(cx, |buffer_store, cx| {
+ buffer_store.wait_for_remote_buffer(buffer_id, cx)
+ })
})?
.await?;
let _ = tx.send(buffer).await;
@@ -3998,7 +4007,11 @@ impl Project {
.map(RepoPath::new)
.collect();
- repository_handle.stage_entries(entries).await?;
+ repository_handle
+ .update(&mut cx, |repository_handle, _| {
+ repository_handle.stage_entries(entries)
+ })?
+ .await??;
Ok(proto::Ack {})
}
@@ -4020,7 +4033,11 @@ impl Project {
.map(RepoPath::new)
.collect();
- repository_handle.unstage_entries(entries).await?;
+ repository_handle
+ .update(&mut cx, |repository_handle, _| {
+ repository_handle.unstage_entries(entries)
+ })?
+ .await??;
Ok(proto::Ack {})
}
@@ -4034,9 +4051,14 @@ impl Project {
let repository_handle =
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
+ let message = SharedString::from(envelope.payload.message);
let name = envelope.payload.name.map(SharedString::from);
let email = envelope.payload.email.map(SharedString::from);
- repository_handle.commit(name.zip(email)).await?;
+ repository_handle
+ .update(&mut cx, |repository_handle, _| {
+ repository_handle.commit(message, name.zip(email))
+ })?
+ .await??;
Ok(proto::Ack {})
}
@@ -4049,55 +4071,12 @@ impl Project {
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 git_repository = match &repository_handle.git_repo {
- git::GitRepo::Local(git_repository) => git_repository.clone(),
- git::GitRepo::Remote { .. } => {
- anyhow::bail!("Cannot handle open commit message buffer for remote git repo")
- }
- };
- let commit_message_file = git_repository.dot_git_dir().join(*COMMIT_MESSAGE);
- let fs = this.update(&mut cx, |project, _| project.fs().clone())?;
- 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 (worktree, relative_path) = this
- .update(&mut cx, |headless_project, cx| {
- headless_project
- .worktree_store
- .update(cx, |worktree_store, cx| {
- worktree_store.find_or_create_worktree(&commit_message_file, false, cx)
- })
- })?
- .await
- .with_context(|| {
- format!("deriving worktree for commit message file {commit_message_file:?}")
- })?;
-
- let buffer = this
- .update(&mut cx, |headless_project, cx| {
- headless_project
- .buffer_store
- .update(cx, |buffer_store, cx| {
- buffer_store.open_buffer(
- ProjectPath {
- worktree_id: worktree.read(cx).id(),
- path: Arc::from(relative_path),
- },
- cx,
- )
- })
- })
- .with_context(|| {
- format!("opening buffer for commit message file {commit_message_file:?}")
+ let buffer = repository_handle
+ .update(&mut cx, |repository_handle, cx| {
+ repository_handle.open_commit_buffer(None, this.read(cx).buffer_store.clone(), cx)
})?
.await?;
+
let peer_id = envelope.original_sender_id()?;
Project::respond_to_open_buffer_request(this, buffer, peer_id, &mut cx)
}
@@ -4107,7 +4086,7 @@ impl Project {
worktree_id: WorktreeId,
work_directory_id: ProjectEntryId,
cx: &mut AsyncApp,
- ) -> Result<RepositoryHandle> {
+ ) -> Result<Entity<Repository>> {
this.update(cx, |project, cx| {
let repository_handle = project
.git_state()
@@ -4115,6 +4094,7 @@ impl Project {
.all_repositories()
.into_iter()
.find(|repository_handle| {
+ let repository_handle = repository_handle.read(cx);
repository_handle.worktree_id == worktree_id
&& repository_handle.repository_entry.work_directory_id()
== work_directory_id
@@ -4160,16 +4140,6 @@ impl Project {
buffer.read(cx).remote_id()
}
- pub fn wait_for_remote_buffer(
- &mut self,
- id: BufferId,
- cx: &mut Context<Self>,
- ) -> Task<Result<Entity<Buffer>>> {
- self.buffer_store.update(cx, |buffer_store, cx| {
- buffer_store.wait_for_remote_buffer(id, cx)
- })
- }
-
fn synchronize_remote_buffers(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
let project_id = match self.client_state {
ProjectClientState::Remote {
@@ -4329,11 +4299,11 @@ impl Project {
&self.git_state
}
- pub fn active_repository(&self, cx: &App) -> Option<RepositoryHandle> {
+ pub fn active_repository(&self, cx: &App) -> Option<Entity<Repository>> {
self.git_state.read(cx).active_repository()
}
- pub fn all_repositories(&self, cx: &App) -> Vec<RepositoryHandle> {
+ pub fn all_repositories(&self, cx: &App) -> Vec<Entity<Repository>> {
self.git_state.read(cx).all_repositories()
}
}
@@ -2693,6 +2693,7 @@ message Commit {
uint64 work_directory_id = 3;
optional string name = 4;
optional string email = 5;
+ string message = 6;
}
message OpenCommitMessageBuffer {
@@ -1,15 +1,15 @@
use anyhow::{anyhow, Context as _, Result};
use extension::ExtensionHostProxy;
use extension_host::headless_host::HeadlessExtensionStore;
-use fs::{CreateOptions, Fs};
-use git::{repository::RepoPath, COMMIT_MESSAGE};
+use fs::Fs;
+use git::repository::RepoPath;
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, PromptLevel, SharedString};
use http_client::HttpClient;
use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry};
use node_runtime::NodeRuntime;
use project::{
buffer_store::{BufferStore, BufferStoreEvent},
- git::{GitRepo, GitState, RepositoryHandle},
+ git::{GitState, Repository},
project_settings::SettingsObserver,
search::SearchQuery,
task_store::TaskStore,
@@ -635,7 +635,11 @@ impl HeadlessProject {
.map(RepoPath::new)
.collect();
- repository_handle.stage_entries(entries).await?;
+ repository_handle
+ .update(&mut cx, |repository_handle, _| {
+ repository_handle.stage_entries(entries)
+ })?
+ .await??;
Ok(proto::Ack {})
}
@@ -657,7 +661,11 @@ impl HeadlessProject {
.map(RepoPath::new)
.collect();
- repository_handle.unstage_entries(entries).await?;
+ repository_handle
+ .update(&mut cx, |repository_handle, _| {
+ repository_handle.unstage_entries(entries)
+ })?
+ .await??;
Ok(proto::Ack {})
}
@@ -672,10 +680,15 @@ impl HeadlessProject {
let repository_handle =
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
+ let message = SharedString::from(envelope.payload.message);
let name = envelope.payload.name.map(SharedString::from);
let email = envelope.payload.email.map(SharedString::from);
- repository_handle.commit(name.zip(email)).await?;
+ repository_handle
+ .update(&mut cx, |repository_handle, _| {
+ repository_handle.commit(message, name.zip(email))
+ })?
+ .await??;
Ok(proto::Ack {})
}
@@ -686,55 +699,11 @@ impl HeadlessProject {
) -> Result<proto::OpenBufferResponse> {
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 =
+ let repository =
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
- let git_repository = match &repository_handle.git_repo {
- GitRepo::Local(git_repository) => git_repository.clone(),
- GitRepo::Remote { .. } => {
- anyhow::bail!("Cannot handle open commit message buffer for remote git repo")
- }
- };
- let commit_message_file = git_repository.dot_git_dir().join(*COMMIT_MESSAGE);
- let fs = this.update(&mut cx, |headless_project, _| headless_project.fs.clone())?;
- 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 (worktree, relative_path) = this
- .update(&mut cx, |headless_project, cx| {
- headless_project
- .worktree_store
- .update(cx, |worktree_store, cx| {
- worktree_store.find_or_create_worktree(&commit_message_file, false, cx)
- })
- })?
- .await
- .with_context(|| {
- format!("deriving worktree for commit message file {commit_message_file:?}")
- })?;
-
- let buffer = this
- .update(&mut cx, |headless_project, cx| {
- headless_project
- .buffer_store
- .update(cx, |buffer_store, cx| {
- buffer_store.open_buffer(
- ProjectPath {
- worktree_id: worktree.read(cx).id(),
- path: Arc::from(relative_path),
- },
- cx,
- )
- })
- })
- .with_context(|| {
- format!("opening buffer for commit message file {commit_message_file:?}")
+ let buffer = repository
+ .update(&mut cx, |repository, cx| {
+ repository.open_commit_buffer(None, this.read(cx).buffer_store.clone(), cx)
})?
.await?;
@@ -759,7 +728,7 @@ impl HeadlessProject {
worktree_id: WorktreeId,
work_directory_id: ProjectEntryId,
cx: &mut AsyncApp,
- ) -> Result<RepositoryHandle> {
+ ) -> Result<Entity<Repository>> {
this.update(cx, |project, cx| {
let repository_handle = project
.git_state
@@ -767,8 +736,11 @@ impl HeadlessProject {
.all_repositories()
.into_iter()
.find(|repository_handle| {
- repository_handle.worktree_id == worktree_id
- && repository_handle.repository_entry.work_directory_id()
+ repository_handle.read(cx).worktree_id == worktree_id
+ && repository_handle
+ .read(cx)
+ .repository_entry
+ .work_directory_id()
== work_directory_id
})
.context("missing repository handle")?;
@@ -199,7 +199,7 @@ pub struct RepositoryEntry {
/// - my_sub_folder_1/project_root/changed_file_1
/// - my_sub_folder_2/changed_file_2
pub(crate) statuses_by_path: SumTree<StatusEntry>,
- pub work_directory_id: ProjectEntryId,
+ work_directory_id: ProjectEntryId,
pub work_directory: WorkDirectory,
pub(crate) branch: Option<Arc<str>>,
}