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:
parent
da4bad3a55
commit
868e3f75b2
17 changed files with 428 additions and 372 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
@ -5254,7 +5254,6 @@ dependencies = [
|
||||||
"picker",
|
"picker",
|
||||||
"postage",
|
"postage",
|
||||||
"project",
|
"project",
|
||||||
"rpc",
|
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
|
@ -7008,6 +7007,7 @@ dependencies = [
|
||||||
"tree-sitter-cpp",
|
"tree-sitter-cpp",
|
||||||
"tree-sitter-css",
|
"tree-sitter-css",
|
||||||
"tree-sitter-diff",
|
"tree-sitter-diff",
|
||||||
|
"tree-sitter-gitcommit",
|
||||||
"tree-sitter-go",
|
"tree-sitter-go",
|
||||||
"tree-sitter-gomod",
|
"tree-sitter-gomod",
|
||||||
"tree-sitter-gowork",
|
"tree-sitter-gowork",
|
||||||
|
@ -13944,6 +13944,15 @@ dependencies = [
|
||||||
"tree-sitter-language",
|
"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]]
|
[[package]]
|
||||||
name = "tree-sitter-go"
|
name = "tree-sitter-go"
|
||||||
version = "0.23.4"
|
version = "0.23.4"
|
||||||
|
|
|
@ -522,6 +522,7 @@ tree-sitter-cpp = "0.23"
|
||||||
tree-sitter-css = "0.23"
|
tree-sitter-css = "0.23"
|
||||||
tree-sitter-elixir = "0.3"
|
tree-sitter-elixir = "0.3"
|
||||||
tree-sitter-embedded-template = "0.23.0"
|
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 = "0.23"
|
||||||
tree-sitter-go-mod = { git = "https://github.com/camdencheek/tree-sitter-go-mod", rev = "6efb59652d30e0e9cd5f3b3a669afd6f1a926d3c", package = "tree-sitter-gomod" }
|
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" }
|
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::status::FileStatus;
|
||||||
|
use crate::GitHostingProviderRegistry;
|
||||||
use crate::{blame::Blame, status::GitStatus};
|
use crate::{blame::Blame, status::GitStatus};
|
||||||
use crate::{GitHostingProviderRegistry, COMMIT_MESSAGE};
|
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use git2::BranchType;
|
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.
|
/// 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, 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 {
|
impl std::fmt::Debug for dyn GitRepository {
|
||||||
|
@ -298,22 +298,14 @@ impl GitRepository for RealGitRepository {
|
||||||
Ok(())
|
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
|
let working_directory = self
|
||||||
.repository
|
.repository
|
||||||
.lock()
|
.lock()
|
||||||
.workdir()
|
.workdir()
|
||||||
.context("failed to read git work directory")?
|
.context("failed to read git work directory")?
|
||||||
.to_path_buf();
|
.to_path_buf();
|
||||||
let commit_file = self.dot_git_dir().join(*COMMIT_MESSAGE);
|
let mut args = vec!["commit", "--quiet", "-m", message, "--cleanup=strip"];
|
||||||
let commit_file_path = commit_file.to_string_lossy();
|
|
||||||
let mut args = vec![
|
|
||||||
"commit",
|
|
||||||
"--quiet",
|
|
||||||
"-F",
|
|
||||||
commit_file_path.as_ref(),
|
|
||||||
"--cleanup=strip",
|
|
||||||
];
|
|
||||||
let author = name_and_email.map(|(name, email)| format!("{name} <{email}>"));
|
let author = name_and_email.map(|(name, email)| format!("{name} <{email}>"));
|
||||||
if let Some(author) = author.as_deref() {
|
if let Some(author) = author.as_deref() {
|
||||||
args.push("--author");
|
args.push("--author");
|
||||||
|
@ -480,7 +472,7 @@ impl GitRepository for FakeGitRepository {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commit(&self, _name_and_email: Option<(&str, &str)>) -> Result<()> {
|
fn commit(&self, _message: &str, _name_and_email: Option<(&str, &str)>) -> Result<()> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,6 @@ multi_buffer.workspace = true
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
rpc.workspace = true
|
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_derive.workspace = true
|
serde_derive.workspace = true
|
||||||
|
|
|
@ -4,7 +4,7 @@ use crate::ProjectDiff;
|
||||||
use crate::{
|
use crate::{
|
||||||
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
|
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
|
||||||
};
|
};
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::Result;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use editor::actions::MoveToEnd;
|
use editor::actions::MoveToEnd;
|
||||||
|
@ -12,13 +12,12 @@ use editor::scroll::ScrollbarAutoHide;
|
||||||
use editor::{Editor, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar};
|
use editor::{Editor, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar};
|
||||||
use git::repository::RepoPath;
|
use git::repository::RepoPath;
|
||||||
use git::status::FileStatus;
|
use git::status::FileStatus;
|
||||||
use git::{CommitAllChanges, CommitChanges, ToggleStaged, COMMIT_MESSAGE};
|
use git::{CommitAllChanges, CommitChanges, ToggleStaged};
|
||||||
use gpui::*;
|
use gpui::*;
|
||||||
use language::{Buffer, BufferId};
|
use language::Buffer;
|
||||||
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
|
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
|
||||||
use project::git::{GitEvent, GitRepo, RepositoryHandle};
|
use project::git::{GitEvent, Repository};
|
||||||
use project::{CreateOptions, Fs, Project, ProjectPath};
|
use project::{Fs, Project, ProjectPath};
|
||||||
use rpc::proto;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::Settings as _;
|
use settings::Settings as _;
|
||||||
use std::{collections::HashSet, path::PathBuf, sync::Arc, time::Duration, usize};
|
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::Toast;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
dock::{DockPosition, Panel, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
Item, Workspace,
|
Workspace,
|
||||||
};
|
};
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
|
@ -144,7 +143,7 @@ pub struct GitPanel {
|
||||||
pending_serialization: Task<Option<()>>,
|
pending_serialization: Task<Option<()>>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
active_repository: Option<RepositoryHandle>,
|
active_repository: Option<Entity<Repository>>,
|
||||||
scroll_handle: UniformListScrollHandle,
|
scroll_handle: UniformListScrollHandle,
|
||||||
scrollbar_state: ScrollbarState,
|
scrollbar_state: ScrollbarState,
|
||||||
selected_entry: Option<usize>,
|
selected_entry: Option<usize>,
|
||||||
|
@ -162,63 +161,6 @@ pub struct GitPanel {
|
||||||
can_commit_all: bool,
|
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(
|
fn commit_message_editor(
|
||||||
commit_message_buffer: Option<Entity<Buffer>>,
|
commit_message_buffer: Option<Entity<Buffer>>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
|
@ -360,7 +302,7 @@ impl GitPanel {
|
||||||
let Some(git_repo) = self.active_repository.as_ref() else {
|
let Some(git_repo) = self.active_repository.as_ref() else {
|
||||||
return;
|
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;
|
return;
|
||||||
};
|
};
|
||||||
let Some(ix) = self.entries_by_path.get(&repo_path) else {
|
let Some(ix) = self.entries_by_path.get(&repo_path) else {
|
||||||
|
@ -578,7 +520,7 @@ impl GitPanel {
|
||||||
.active_repository
|
.active_repository
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(false, |active_repository| {
|
.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() {
|
if have_entries && self.selected_entry.is_none() {
|
||||||
self.selected_entry = Some(0);
|
self.selected_entry = Some(0);
|
||||||
|
@ -655,11 +597,17 @@ impl GitPanel {
|
||||||
let repo_paths = repo_paths.clone();
|
let repo_paths = repo_paths.clone();
|
||||||
let active_repository = active_repository.clone();
|
let active_repository = active_repository.clone();
|
||||||
|this, mut cx| async move {
|
|this, mut cx| async move {
|
||||||
let result = if stage {
|
let result = cx
|
||||||
active_repository.stage_entries(repo_paths.clone()).await
|
.update(|cx| {
|
||||||
|
if stage {
|
||||||
|
active_repository.read(cx).stage_entries(repo_paths.clone())
|
||||||
} else {
|
} else {
|
||||||
active_repository.unstage_entries(repo_paths.clone()).await
|
active_repository
|
||||||
};
|
.read(cx)
|
||||||
|
.unstage_entries(repo_paths.clone())
|
||||||
|
}
|
||||||
|
})?
|
||||||
|
.await?;
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
for pending in this.pending.iter_mut() {
|
for pending in this.pending.iter_mut() {
|
||||||
|
@ -697,7 +645,9 @@ impl GitPanel {
|
||||||
let Some(active_repository) = self.active_repository.as_ref() else {
|
let Some(active_repository) = self.active_repository.as_ref() else {
|
||||||
return;
|
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 {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
@ -725,18 +675,18 @@ impl GitPanel {
|
||||||
if !self.can_commit {
|
if !self.can_commit {
|
||||||
return;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
self.commit_pending = true;
|
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 commit_editor = self.commit_editor.clone();
|
||||||
self.commit_task = cx.spawn_in(window, |git_panel, mut cx| async move {
|
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 {
|
let result = maybe!(async {
|
||||||
save_task.await?;
|
commit.await??;
|
||||||
active_repository.commit(name_and_email).await?;
|
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
commit_editor.update(cx, |editor, cx| editor.clear(window, cx));
|
commit_editor.update(cx, |editor, cx| editor.clear(window, cx));
|
||||||
})
|
})
|
||||||
|
@ -768,14 +718,12 @@ impl GitPanel {
|
||||||
if !self.can_commit_all {
|
if !self.can_commit_all {
|
||||||
return;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
self.commit_pending = true;
|
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 commit_editor = self.commit_editor.clone();
|
||||||
let tracked_files = self
|
let tracked_files = self
|
||||||
.entries
|
.entries
|
||||||
|
@ -790,9 +738,15 @@ impl GitPanel {
|
||||||
|
|
||||||
self.commit_task = cx.spawn_in(window, |git_panel, mut cx| async move {
|
self.commit_task = cx.spawn_in(window, |git_panel, mut cx| async move {
|
||||||
let result = maybe!(async {
|
let result = maybe!(async {
|
||||||
save_task.await?;
|
cx.update(|_, cx| active_repository.read(cx).stage_entries(tracked_files))?
|
||||||
active_repository.stage_entries(tracked_files).await?;
|
.await??;
|
||||||
active_repository.commit(name_and_email).await
|
cx.update(|_, cx| {
|
||||||
|
active_repository
|
||||||
|
.read(cx)
|
||||||
|
.commit(SharedString::from(message), name_and_email)
|
||||||
|
})?
|
||||||
|
.await??;
|
||||||
|
Ok(())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
cx.update(|window, cx| match result {
|
cx.update(|window, cx| match result {
|
||||||
|
@ -886,47 +840,56 @@ impl GitPanel {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let project = self.project.clone();
|
|
||||||
let handle = cx.entity().downgrade();
|
let handle = cx.entity().downgrade();
|
||||||
|
self.reopen_commit_buffer(window, cx);
|
||||||
self.update_visible_entries_task = cx.spawn_in(window, |_, mut cx| async move {
|
self.update_visible_entries_task = cx.spawn_in(window, |_, mut cx| async move {
|
||||||
cx.background_executor().timer(UPDATE_DEBOUNCE).await;
|
cx.background_executor().timer(UPDATE_DEBOUNCE).await;
|
||||||
if let Some(git_panel) = handle.upgrade() {
|
if let Some(git_panel) = handle.upgrade() {
|
||||||
let Ok(commit_message_buffer) = git_panel.update_in(&mut cx, |git_panel, _, cx| {
|
|
||||||
git_panel
|
git_panel
|
||||||
.active_repository
|
.update_in(&mut cx, |git_panel, _, cx| {
|
||||||
.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);
|
|
||||||
if clear_pending {
|
if clear_pending {
|
||||||
git_panel.clear_pending();
|
git_panel.clear_pending();
|
||||||
}
|
}
|
||||||
git_panel.commit_editor =
|
git_panel.update_visible_entries(cx);
|
||||||
cx.new(|cx| commit_message_editor(commit_message_buffer, window, cx));
|
|
||||||
})
|
})
|
||||||
.ok();
|
.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) {
|
fn clear_pending(&mut self) {
|
||||||
self.pending.retain(|v| !v.finished)
|
self.pending.retain(|v| !v.finished)
|
||||||
}
|
}
|
||||||
|
@ -944,6 +907,7 @@ impl GitPanel {
|
||||||
};
|
};
|
||||||
|
|
||||||
// First pass - collect all paths
|
// First pass - collect all paths
|
||||||
|
let repo = repo.read(cx);
|
||||||
let path_set = HashSet::from_iter(repo.status().map(|entry| entry.repo_path));
|
let path_set = HashSet::from_iter(repo.status().map(|entry| entry.repo_path));
|
||||||
|
|
||||||
let mut has_changed_checked_boxes = false;
|
let mut has_changed_checked_boxes = false;
|
||||||
|
@ -1117,7 +1081,7 @@ impl GitPanel {
|
||||||
let entry_count = self
|
let entry_count = self
|
||||||
.active_repository
|
.active_repository
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(0, RepositoryHandle::entry_count);
|
.map_or(0, |repo| repo.read(cx).entry_count());
|
||||||
|
|
||||||
let changes_string = match entry_count {
|
let changes_string = match entry_count {
|
||||||
0 => "No changes".to_string(),
|
0 => "No changes".to_string(),
|
||||||
|
@ -1151,7 +1115,7 @@ impl GitPanel {
|
||||||
let active_repository = self.project.read(cx).active_repository(cx);
|
let active_repository = self.project.read(cx).active_repository(cx);
|
||||||
let repository_display_name = active_repository
|
let repository_display_name = active_repository
|
||||||
.as_ref()
|
.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();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let entry_count = self.entries.len();
|
let entry_count = self.entries.len();
|
||||||
|
@ -1619,7 +1583,7 @@ impl Render for GitPanel {
|
||||||
.active_repository
|
.active_repository
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(false, |active_repository| {
|
.map_or(false, |active_repository| {
|
||||||
active_repository.entry_count() > 0
|
active_repository.read(cx).entry_count() > 0
|
||||||
});
|
});
|
||||||
let room = self
|
let room = self
|
||||||
.workspace
|
.workspace
|
||||||
|
|
|
@ -163,6 +163,7 @@ impl ProjectDiff {
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(path) = git_repo
|
let Some(path) = git_repo
|
||||||
|
.read(cx)
|
||||||
.repo_path_to_project_path(&entry.repo_path)
|
.repo_path_to_project_path(&entry.repo_path)
|
||||||
.and_then(|project_path| self.project.read(cx).absolute_path(&project_path, cx))
|
.and_then(|project_path| self.project.read(cx).absolute_path(&project_path, cx))
|
||||||
else {
|
else {
|
||||||
|
@ -234,6 +235,7 @@ impl ProjectDiff {
|
||||||
let mut previous_paths = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
|
let mut previous_paths = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
|
||||||
|
|
||||||
let mut result = vec![];
|
let mut result = vec![];
|
||||||
|
repo.update(cx, |repo, cx| {
|
||||||
for entry in repo.status() {
|
for entry in repo.status() {
|
||||||
if !entry.status.has_changes() {
|
if !entry.status.has_changes() {
|
||||||
continue;
|
continue;
|
||||||
|
@ -271,6 +273,7 @@ impl ProjectDiff {
|
||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
self.multibuffer.update(cx, |multibuffer, cx| {
|
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||||
for path in previous_paths {
|
for path in previous_paths {
|
||||||
multibuffer.remove_excerpts_for_path(path, cx);
|
multibuffer.remove_excerpts_for_path(path, cx);
|
||||||
|
|
|
@ -4,7 +4,7 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::{
|
use project::{
|
||||||
git::{GitState, RepositoryHandle},
|
git::{GitState, Repository},
|
||||||
Project,
|
Project,
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -117,13 +117,13 @@ impl<T: PopoverTrigger> RenderOnce for RepositorySelectorPopoverMenu<T> {
|
||||||
pub struct RepositorySelectorDelegate {
|
pub struct RepositorySelectorDelegate {
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
repository_selector: WeakEntity<RepositorySelector>,
|
repository_selector: WeakEntity<RepositorySelector>,
|
||||||
repository_entries: Vec<RepositoryHandle>,
|
repository_entries: Vec<Entity<Repository>>,
|
||||||
filtered_repositories: Vec<RepositoryHandle>,
|
filtered_repositories: Vec<Entity<Repository>>,
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RepositorySelectorDelegate {
|
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.repository_entries = all_repositories.clone();
|
||||||
self.filtered_repositories = all_repositories;
|
self.filtered_repositories = all_repositories;
|
||||||
self.selected_index = 0;
|
self.selected_index = 0;
|
||||||
|
@ -194,7 +194,7 @@ impl PickerDelegate for RepositorySelectorDelegate {
|
||||||
let Some(selected_repo) = self.filtered_repositories.get(self.selected_index) else {
|
let Some(selected_repo) = self.filtered_repositories.get(self.selected_index) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
selected_repo.activate(cx);
|
selected_repo.update(cx, |selected_repo, cx| selected_repo.activate(cx));
|
||||||
self.dismissed(window, cx);
|
self.dismissed(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,7 +222,7 @@ impl PickerDelegate for RepositorySelectorDelegate {
|
||||||
) -> Option<Self::ListItem> {
|
) -> Option<Self::ListItem> {
|
||||||
let project = self.project.upgrade()?;
|
let project = self.project.upgrade()?;
|
||||||
let repo_info = self.filtered_repositories.get(ix)?;
|
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
|
// TODO: Implement repository item rendering
|
||||||
Some(
|
Some(
|
||||||
ListItem::new(ix)
|
ListItem::new(ix)
|
||||||
|
|
|
@ -19,6 +19,7 @@ load-grammars = [
|
||||||
"tree-sitter-cpp",
|
"tree-sitter-cpp",
|
||||||
"tree-sitter-css",
|
"tree-sitter-css",
|
||||||
"tree-sitter-diff",
|
"tree-sitter-diff",
|
||||||
|
"tree-sitter-gitcommit",
|
||||||
"tree-sitter-go",
|
"tree-sitter-go",
|
||||||
"tree-sitter-go-mod",
|
"tree-sitter-go-mod",
|
||||||
"tree-sitter-gowork",
|
"tree-sitter-gowork",
|
||||||
|
@ -69,6 +70,7 @@ tree-sitter-c = { workspace = true, optional = true }
|
||||||
tree-sitter-cpp = { workspace = true, optional = true }
|
tree-sitter-cpp = { workspace = true, optional = true }
|
||||||
tree-sitter-css = { workspace = true, optional = true }
|
tree-sitter-css = { workspace = true, optional = true }
|
||||||
tree-sitter-diff = { 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 = { workspace = true, optional = true }
|
||||||
tree-sitter-go-mod = { workspace = true, optional = true }
|
tree-sitter-go-mod = { workspace = true, optional = true }
|
||||||
tree-sitter-gowork = { workspace = true, optional = true }
|
tree-sitter-gowork = { workspace = true, optional = true }
|
||||||
|
|
18
crates/languages/src/gitcommit/config.toml
Normal file
18
crates/languages/src/gitcommit/config.toml
Normal file
|
@ -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 },
|
||||||
|
]
|
18
crates/languages/src/gitcommit/highlights.scm
Normal file
18
crates/languages/src/gitcommit/highlights.scm
Normal file
|
@ -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
|
5
crates/languages/src/gitcommit/injections.scm
Normal file
5
crates/languages/src/gitcommit/injections.scm
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
((scissors) @content
|
||||||
|
(#set! "language" "diff"))
|
||||||
|
|
||||||
|
((rebase_command) @content
|
||||||
|
(#set! "language" "git_rebase"))
|
|
@ -31,6 +31,25 @@ mod yaml;
|
||||||
#[exclude = "*.rs"]
|
#[exclude = "*.rs"]
|
||||||
struct LanguageDir;
|
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) {
|
pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mut App) {
|
||||||
#[cfg(feature = "load-grammars")]
|
#[cfg(feature = "load-grammars")]
|
||||||
languages.register_native_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),
|
("tsx", tree_sitter_typescript::LANGUAGE_TSX),
|
||||||
("typescript", tree_sitter_typescript::LANGUAGE_TYPESCRIPT),
|
("typescript", tree_sitter_typescript::LANGUAGE_TYPESCRIPT),
|
||||||
("yaml", tree_sitter_yaml::LANGUAGE),
|
("yaml", tree_sitter_yaml::LANGUAGE),
|
||||||
|
("gitcommit", tree_sitter_gitcommit::LANGUAGE),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
macro_rules! language {
|
macro_rules! language {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
use crate::buffer_store::BufferStore;
|
||||||
use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
|
use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
|
||||||
use crate::{Project, ProjectPath};
|
use crate::{Project, ProjectPath};
|
||||||
use anyhow::{anyhow, Context as _};
|
use anyhow::Context as _;
|
||||||
use client::ProjectId;
|
use client::ProjectId;
|
||||||
use futures::channel::{mpsc, oneshot};
|
use futures::channel::{mpsc, oneshot};
|
||||||
use futures::StreamExt as _;
|
use futures::StreamExt as _;
|
||||||
|
@ -8,24 +9,28 @@ use git::{
|
||||||
repository::{GitRepository, RepoPath},
|
repository::{GitRepository, RepoPath},
|
||||||
status::{GitSummary, TrackedSummary},
|
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 rpc::{proto, AnyProtoClient};
|
||||||
use settings::WorktreeId;
|
use settings::WorktreeId;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use text::BufferId;
|
||||||
use util::{maybe, ResultExt};
|
use util::{maybe, ResultExt};
|
||||||
use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry};
|
use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry};
|
||||||
|
|
||||||
pub struct GitState {
|
pub struct GitState {
|
||||||
project_id: Option<ProjectId>,
|
project_id: Option<ProjectId>,
|
||||||
client: Option<AnyProtoClient>,
|
client: Option<AnyProtoClient>,
|
||||||
repositories: Vec<RepositoryHandle>,
|
repositories: Vec<Entity<Repository>>,
|
||||||
active_index: Option<usize>,
|
active_index: Option<usize>,
|
||||||
update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<anyhow::Result<()>>)>,
|
update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<anyhow::Result<()>>)>,
|
||||||
_subscription: Subscription,
|
_subscription: Subscription,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
pub struct Repository {
|
||||||
pub struct RepositoryHandle {
|
commit_message_buffer: Option<Entity<Buffer>>,
|
||||||
git_state: WeakEntity<GitState>,
|
git_state: WeakEntity<GitState>,
|
||||||
pub worktree_id: WorktreeId,
|
pub worktree_id: WorktreeId,
|
||||||
pub repository_entry: RepositoryEntry,
|
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 {
|
enum Message {
|
||||||
Commit {
|
Commit {
|
||||||
git_repo: GitRepo,
|
git_repo: GitRepo,
|
||||||
|
message: SharedString,
|
||||||
name_and_email: Option<(SharedString, SharedString)>,
|
name_and_email: Option<(SharedString, SharedString)>,
|
||||||
},
|
},
|
||||||
Stage(GitRepo, Vec<RepoPath>),
|
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
|
self.active_index
|
||||||
.map(|index| self.repositories[index].clone())
|
.map(|index| self.repositories[index].clone())
|
||||||
}
|
}
|
||||||
|
@ -118,7 +108,7 @@ impl GitState {
|
||||||
|
|
||||||
worktree_store.update(cx, |worktree_store, cx| {
|
worktree_store.update(cx, |worktree_store, cx| {
|
||||||
for worktree in worktree_store.worktrees() {
|
for worktree in worktree_store.worktrees() {
|
||||||
worktree.update(cx, |worktree, _| {
|
worktree.update(cx, |worktree, cx| {
|
||||||
let snapshot = worktree.snapshot();
|
let snapshot = worktree.snapshot();
|
||||||
for repo in snapshot.repositories().iter() {
|
for repo in snapshot.repositories().iter() {
|
||||||
let git_repo = worktree
|
let git_repo = worktree
|
||||||
|
@ -139,27 +129,34 @@ impl GitState {
|
||||||
let Some(git_repo) = git_repo else {
|
let Some(git_repo) = git_repo else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let existing = self
|
let worktree_id = worktree.id();
|
||||||
.repositories
|
let existing =
|
||||||
|
self.repositories
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.find(|(_, existing_handle)| existing_handle == &repo);
|
.find(|(_, existing_handle)| {
|
||||||
|
existing_handle.read(cx).id()
|
||||||
|
== (worktree_id, repo.work_directory_id())
|
||||||
|
});
|
||||||
let handle = if let Some((index, handle)) = existing {
|
let handle = if let Some((index, handle)) = existing {
|
||||||
if self.active_index == Some(index) {
|
if self.active_index == Some(index) {
|
||||||
new_active_index = Some(new_repositories.len());
|
new_active_index = Some(new_repositories.len());
|
||||||
}
|
}
|
||||||
// Update the statuses but keep everything else.
|
// Update the statuses but keep everything else.
|
||||||
let mut existing_handle = handle.clone();
|
let existing_handle = handle.clone();
|
||||||
|
existing_handle.update(cx, |existing_handle, _| {
|
||||||
existing_handle.repository_entry = repo.clone();
|
existing_handle.repository_entry = repo.clone();
|
||||||
|
});
|
||||||
existing_handle
|
existing_handle
|
||||||
} else {
|
} else {
|
||||||
RepositoryHandle {
|
cx.new(|_| Repository {
|
||||||
git_state: this.clone(),
|
git_state: this.clone(),
|
||||||
worktree_id: worktree.id(),
|
worktree_id,
|
||||||
repository_entry: repo.clone(),
|
repository_entry: repo.clone(),
|
||||||
git_repo,
|
git_repo,
|
||||||
update_sender: self.update_sender.clone(),
|
update_sender: self.update_sender.clone(),
|
||||||
}
|
commit_message_buffer: None,
|
||||||
|
})
|
||||||
};
|
};
|
||||||
new_repositories.push(handle);
|
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()
|
self.repositories.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -260,10 +257,12 @@ impl GitState {
|
||||||
}
|
}
|
||||||
Message::Commit {
|
Message::Commit {
|
||||||
git_repo,
|
git_repo,
|
||||||
|
message,
|
||||||
name_and_email,
|
name_and_email,
|
||||||
} => {
|
} => {
|
||||||
match git_repo {
|
match git_repo {
|
||||||
GitRepo::Local(repo) => repo.commit(
|
GitRepo::Local(repo) => repo.commit(
|
||||||
|
message.as_ref(),
|
||||||
name_and_email
|
name_and_email
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|(name, email)| (name.as_ref(), email.as_ref())),
|
.map(|(name, email)| (name.as_ref(), email.as_ref())),
|
||||||
|
@ -280,6 +279,7 @@ impl GitState {
|
||||||
project_id: project_id.0,
|
project_id: project_id.0,
|
||||||
worktree_id: worktree_id.to_proto(),
|
worktree_id: worktree_id.to_proto(),
|
||||||
work_directory_id: work_directory_id.to_proto(),
|
work_directory_id: work_directory_id.to_proto(),
|
||||||
|
message: String::from(message),
|
||||||
name: name.map(String::from),
|
name: name.map(String::from),
|
||||||
email: email.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 {
|
pub fn display_name(&self, project: &Project, cx: &App) -> SharedString {
|
||||||
maybe!({
|
maybe!({
|
||||||
let path = self.repo_path_to_project_path(&"".into())?;
|
let path = self.repo_path_to_project_path(&"".into())?;
|
||||||
|
@ -318,7 +322,7 @@ impl RepositoryHandle {
|
||||||
.repositories
|
.repositories
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.find(|(_, handle)| handle == &self)
|
.find(|(_, handle)| handle.read(cx).id() == self.id())
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
@ -343,47 +347,121 @@ impl RepositoryHandle {
|
||||||
self.repository_entry.relativize(&path.path).log_err()
|
self.repository_entry.relativize(&path.path).log_err()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn stage_entries(&self, entries: Vec<RepoPath>) -> anyhow::Result<()> {
|
pub fn open_commit_buffer(
|
||||||
if entries.is_empty() {
|
&mut self,
|
||||||
return Ok(());
|
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();
|
let (result_tx, result_rx) = futures::channel::oneshot::channel();
|
||||||
|
if entries.is_empty() {
|
||||||
|
result_tx.send(Ok(())).ok();
|
||||||
|
return result_rx;
|
||||||
|
}
|
||||||
self.update_sender
|
self.update_sender
|
||||||
.unbounded_send((Message::Stage(self.git_repo.clone(), entries), result_tx))
|
.unbounded_send((Message::Stage(self.git_repo.clone(), entries), result_tx))
|
||||||
.map_err(|_| anyhow!("Failed to submit stage operation"))?;
|
.ok();
|
||||||
|
result_rx
|
||||||
result_rx.await?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unstage_entries(&self, entries: Vec<RepoPath>) -> anyhow::Result<()> {
|
pub fn unstage_entries(&self, entries: Vec<RepoPath>) -> oneshot::Receiver<anyhow::Result<()>> {
|
||||||
if entries.is_empty() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let (result_tx, result_rx) = futures::channel::oneshot::channel();
|
let (result_tx, result_rx) = futures::channel::oneshot::channel();
|
||||||
|
if entries.is_empty() {
|
||||||
|
result_tx.send(Ok(())).ok();
|
||||||
|
return result_rx;
|
||||||
|
}
|
||||||
self.update_sender
|
self.update_sender
|
||||||
.unbounded_send((Message::Unstage(self.git_repo.clone(), entries), result_tx))
|
.unbounded_send((Message::Unstage(self.git_repo.clone(), entries), result_tx))
|
||||||
.map_err(|_| anyhow!("Failed to submit unstage operation"))?;
|
.ok();
|
||||||
result_rx.await?
|
result_rx
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn stage_all(&self) -> anyhow::Result<()> {
|
pub fn stage_all(&self) -> oneshot::Receiver<anyhow::Result<()>> {
|
||||||
let to_stage = self
|
let to_stage = self
|
||||||
.repository_entry
|
.repository_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).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
|
let to_unstage = self
|
||||||
.repository_entry
|
.repository_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).await
|
self.unstage_entries(to_unstage)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a count of all entries in the active repository, including
|
/// 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());
|
return self.have_changes() && (commit_all || self.have_staged_changes());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn commit(
|
pub fn commit(
|
||||||
&self,
|
&self,
|
||||||
|
message: SharedString,
|
||||||
name_and_email: Option<(SharedString, SharedString)>,
|
name_and_email: Option<(SharedString, SharedString)>,
|
||||||
) -> anyhow::Result<()> {
|
) -> oneshot::Receiver<anyhow::Result<()>> {
|
||||||
let (result_tx, result_rx) = futures::channel::oneshot::channel();
|
let (result_tx, result_rx) = futures::channel::oneshot::channel();
|
||||||
self.update_sender.unbounded_send((
|
self.update_sender
|
||||||
|
.unbounded_send((
|
||||||
Message::Commit {
|
Message::Commit {
|
||||||
git_repo: self.git_repo.clone(),
|
git_repo: self.git_repo.clone(),
|
||||||
|
message,
|
||||||
name_and_email,
|
name_and_email,
|
||||||
},
|
},
|
||||||
result_tx,
|
result_tx,
|
||||||
))?;
|
))
|
||||||
result_rx.await?
|
.ok();
|
||||||
|
result_rx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ mod project_tests;
|
||||||
mod direnv;
|
mod direnv;
|
||||||
mod environment;
|
mod environment;
|
||||||
pub use environment::EnvironmentErrorMessage;
|
pub use environment::EnvironmentErrorMessage;
|
||||||
use git::RepositoryHandle;
|
use git::Repository;
|
||||||
pub mod search_history;
|
pub mod search_history;
|
||||||
mod yarn;
|
mod yarn;
|
||||||
|
|
||||||
|
@ -48,7 +48,6 @@ use ::git::{
|
||||||
blame::Blame,
|
blame::Blame,
|
||||||
repository::{Branch, GitRepository, RepoPath},
|
repository::{Branch, GitRepository, RepoPath},
|
||||||
status::FileStatus,
|
status::FileStatus,
|
||||||
COMMIT_MESSAGE,
|
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyEntity, App, AppContext as _, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter,
|
AnyEntity, App, AppContext as _, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter,
|
||||||
|
@ -1998,10 +1997,13 @@ impl Project {
|
||||||
project_id,
|
project_id,
|
||||||
id: id.into(),
|
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)?;
|
let buffer_id = BufferId::new(request.await?.buffer_id)?;
|
||||||
this.update(&mut cx, |this, cx| {
|
project
|
||||||
this.wait_for_remote_buffer(buffer_id, cx)
|
.update(&mut cx, |project, cx| {
|
||||||
|
project.buffer_store.update(cx, |buffer_store, cx| {
|
||||||
|
buffer_store.wait_for_remote_buffer(buffer_id, cx)
|
||||||
|
})
|
||||||
})?
|
})?
|
||||||
.await
|
.await
|
||||||
})
|
})
|
||||||
|
@ -2846,16 +2848,21 @@ impl Project {
|
||||||
|
|
||||||
let proto_client = ssh_client.read(cx).proto_client();
|
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
|
let buffer = proto_client
|
||||||
.request(proto::OpenServerSettings {
|
.request(proto::OpenServerSettings {
|
||||||
project_id: SSH_PROJECT_ID,
|
project_id: SSH_PROJECT_ID,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let buffer = this
|
let buffer = project
|
||||||
.update(&mut cx, |this, cx| {
|
.update(&mut cx, |project, cx| {
|
||||||
anyhow::Ok(this.wait_for_remote_buffer(BufferId::new(buffer.buffer_id)?, cx))
|
project.buffer_store.update(cx, |buffer_store, cx| {
|
||||||
|
anyhow::Ok(
|
||||||
|
buffer_store
|
||||||
|
.wait_for_remote_buffer(BufferId::new(buffer.buffer_id)?, cx),
|
||||||
|
)
|
||||||
|
})
|
||||||
})??
|
})??
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -3186,13 +3193,15 @@ impl Project {
|
||||||
});
|
});
|
||||||
let guard = self.retain_remotely_created_models(cx);
|
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?;
|
let response = request.await?;
|
||||||
for buffer_id in response.buffer_ids {
|
for buffer_id in response.buffer_ids {
|
||||||
let buffer_id = BufferId::new(buffer_id)?;
|
let buffer_id = BufferId::new(buffer_id)?;
|
||||||
let buffer = this
|
let buffer = project
|
||||||
.update(&mut cx, |this, cx| {
|
.update(&mut cx, |project, cx| {
|
||||||
this.wait_for_remote_buffer(buffer_id, cx)
|
project.buffer_store.update(cx, |buffer_store, cx| {
|
||||||
|
buffer_store.wait_for_remote_buffer(buffer_id, cx)
|
||||||
|
})
|
||||||
})?
|
})?
|
||||||
.await?;
|
.await?;
|
||||||
let _ = tx.send(buffer).await;
|
let _ = tx.send(buffer).await;
|
||||||
|
@ -3998,7 +4007,11 @@ impl Project {
|
||||||
.map(RepoPath::new)
|
.map(RepoPath::new)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
repository_handle.stage_entries(entries).await?;
|
repository_handle
|
||||||
|
.update(&mut cx, |repository_handle, _| {
|
||||||
|
repository_handle.stage_entries(entries)
|
||||||
|
})?
|
||||||
|
.await??;
|
||||||
Ok(proto::Ack {})
|
Ok(proto::Ack {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4020,7 +4033,11 @@ impl Project {
|
||||||
.map(RepoPath::new)
|
.map(RepoPath::new)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
repository_handle.unstage_entries(entries).await?;
|
repository_handle
|
||||||
|
.update(&mut cx, |repository_handle, _| {
|
||||||
|
repository_handle.unstage_entries(entries)
|
||||||
|
})?
|
||||||
|
.await??;
|
||||||
Ok(proto::Ack {})
|
Ok(proto::Ack {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4034,9 +4051,14 @@ impl Project {
|
||||||
let repository_handle =
|
let repository_handle =
|
||||||
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
|
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 name = envelope.payload.name.map(SharedString::from);
|
||||||
let email = envelope.payload.email.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 {})
|
Ok(proto::Ack {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4049,55 +4071,12 @@ impl Project {
|
||||||
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
|
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
|
||||||
let repository_handle =
|
let repository_handle =
|
||||||
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
|
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
|
||||||
let git_repository = match &repository_handle.git_repo {
|
let buffer = repository_handle
|
||||||
git::GitRepo::Local(git_repository) => git_repository.clone(),
|
.update(&mut cx, |repository_handle, cx| {
|
||||||
git::GitRepo::Remote { .. } => {
|
repository_handle.open_commit_buffer(None, this.read(cx).buffer_store.clone(), cx)
|
||||||
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:?}")
|
|
||||||
})?
|
})?
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let peer_id = envelope.original_sender_id()?;
|
let peer_id = envelope.original_sender_id()?;
|
||||||
Project::respond_to_open_buffer_request(this, buffer, peer_id, &mut cx)
|
Project::respond_to_open_buffer_request(this, buffer, peer_id, &mut cx)
|
||||||
}
|
}
|
||||||
|
@ -4107,7 +4086,7 @@ impl Project {
|
||||||
worktree_id: WorktreeId,
|
worktree_id: WorktreeId,
|
||||||
work_directory_id: ProjectEntryId,
|
work_directory_id: ProjectEntryId,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> Result<RepositoryHandle> {
|
) -> Result<Entity<Repository>> {
|
||||||
this.update(cx, |project, cx| {
|
this.update(cx, |project, cx| {
|
||||||
let repository_handle = project
|
let repository_handle = project
|
||||||
.git_state()
|
.git_state()
|
||||||
|
@ -4115,6 +4094,7 @@ impl Project {
|
||||||
.all_repositories()
|
.all_repositories()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|repository_handle| {
|
.find(|repository_handle| {
|
||||||
|
let repository_handle = repository_handle.read(cx);
|
||||||
repository_handle.worktree_id == worktree_id
|
repository_handle.worktree_id == worktree_id
|
||||||
&& repository_handle.repository_entry.work_directory_id()
|
&& repository_handle.repository_entry.work_directory_id()
|
||||||
== work_directory_id
|
== work_directory_id
|
||||||
|
@ -4160,16 +4140,6 @@ impl Project {
|
||||||
buffer.read(cx).remote_id()
|
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<()>> {
|
fn synchronize_remote_buffers(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||||
let project_id = match self.client_state {
|
let project_id = match self.client_state {
|
||||||
ProjectClientState::Remote {
|
ProjectClientState::Remote {
|
||||||
|
@ -4329,11 +4299,11 @@ impl Project {
|
||||||
&self.git_state
|
&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()
|
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()
|
self.git_state.read(cx).all_repositories()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2693,6 +2693,7 @@ message Commit {
|
||||||
uint64 work_directory_id = 3;
|
uint64 work_directory_id = 3;
|
||||||
optional string name = 4;
|
optional string name = 4;
|
||||||
optional string email = 5;
|
optional string email = 5;
|
||||||
|
string message = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message OpenCommitMessageBuffer {
|
message OpenCommitMessageBuffer {
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use extension::ExtensionHostProxy;
|
use extension::ExtensionHostProxy;
|
||||||
use extension_host::headless_host::HeadlessExtensionStore;
|
use extension_host::headless_host::HeadlessExtensionStore;
|
||||||
use fs::{CreateOptions, Fs};
|
use fs::Fs;
|
||||||
use git::{repository::RepoPath, COMMIT_MESSAGE};
|
use git::repository::RepoPath;
|
||||||
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, PromptLevel, SharedString};
|
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, PromptLevel, SharedString};
|
||||||
use http_client::HttpClient;
|
use http_client::HttpClient;
|
||||||
use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry};
|
use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry};
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
use project::{
|
use project::{
|
||||||
buffer_store::{BufferStore, BufferStoreEvent},
|
buffer_store::{BufferStore, BufferStoreEvent},
|
||||||
git::{GitRepo, GitState, RepositoryHandle},
|
git::{GitState, Repository},
|
||||||
project_settings::SettingsObserver,
|
project_settings::SettingsObserver,
|
||||||
search::SearchQuery,
|
search::SearchQuery,
|
||||||
task_store::TaskStore,
|
task_store::TaskStore,
|
||||||
|
@ -635,7 +635,11 @@ impl HeadlessProject {
|
||||||
.map(RepoPath::new)
|
.map(RepoPath::new)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
repository_handle.stage_entries(entries).await?;
|
repository_handle
|
||||||
|
.update(&mut cx, |repository_handle, _| {
|
||||||
|
repository_handle.stage_entries(entries)
|
||||||
|
})?
|
||||||
|
.await??;
|
||||||
Ok(proto::Ack {})
|
Ok(proto::Ack {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -657,7 +661,11 @@ impl HeadlessProject {
|
||||||
.map(RepoPath::new)
|
.map(RepoPath::new)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
repository_handle.unstage_entries(entries).await?;
|
repository_handle
|
||||||
|
.update(&mut cx, |repository_handle, _| {
|
||||||
|
repository_handle.unstage_entries(entries)
|
||||||
|
})?
|
||||||
|
.await??;
|
||||||
|
|
||||||
Ok(proto::Ack {})
|
Ok(proto::Ack {})
|
||||||
}
|
}
|
||||||
|
@ -672,10 +680,15 @@ impl HeadlessProject {
|
||||||
let repository_handle =
|
let repository_handle =
|
||||||
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
|
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 name = envelope.payload.name.map(SharedString::from);
|
||||||
let email = envelope.payload.email.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 {})
|
Ok(proto::Ack {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -686,55 +699,11 @@ impl HeadlessProject {
|
||||||
) -> Result<proto::OpenBufferResponse> {
|
) -> Result<proto::OpenBufferResponse> {
|
||||||
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
|
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
|
||||||
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_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)?;
|
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
|
||||||
let git_repository = match &repository_handle.git_repo {
|
let buffer = repository
|
||||||
GitRepo::Local(git_repository) => git_repository.clone(),
|
.update(&mut cx, |repository, cx| {
|
||||||
GitRepo::Remote { .. } => {
|
repository.open_commit_buffer(None, this.read(cx).buffer_store.clone(), cx)
|
||||||
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:?}")
|
|
||||||
})?
|
})?
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
@ -759,7 +728,7 @@ impl HeadlessProject {
|
||||||
worktree_id: WorktreeId,
|
worktree_id: WorktreeId,
|
||||||
work_directory_id: ProjectEntryId,
|
work_directory_id: ProjectEntryId,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> Result<RepositoryHandle> {
|
) -> Result<Entity<Repository>> {
|
||||||
this.update(cx, |project, cx| {
|
this.update(cx, |project, cx| {
|
||||||
let repository_handle = project
|
let repository_handle = project
|
||||||
.git_state
|
.git_state
|
||||||
|
@ -767,8 +736,11 @@ impl HeadlessProject {
|
||||||
.all_repositories()
|
.all_repositories()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|repository_handle| {
|
.find(|repository_handle| {
|
||||||
repository_handle.worktree_id == worktree_id
|
repository_handle.read(cx).worktree_id == worktree_id
|
||||||
&& repository_handle.repository_entry.work_directory_id()
|
&& repository_handle
|
||||||
|
.read(cx)
|
||||||
|
.repository_entry
|
||||||
|
.work_directory_id()
|
||||||
== work_directory_id
|
== work_directory_id
|
||||||
})
|
})
|
||||||
.context("missing repository handle")?;
|
.context("missing repository handle")?;
|
||||||
|
|
|
@ -199,7 +199,7 @@ pub struct RepositoryEntry {
|
||||||
/// - my_sub_folder_1/project_root/changed_file_1
|
/// - my_sub_folder_1/project_root/changed_file_1
|
||||||
/// - my_sub_folder_2/changed_file_2
|
/// - my_sub_folder_2/changed_file_2
|
||||||
pub(crate) statuses_by_path: SumTree<StatusEntry>,
|
pub(crate) statuses_by_path: SumTree<StatusEntry>,
|
||||||
pub work_directory_id: ProjectEntryId,
|
work_directory_id: ProjectEntryId,
|
||||||
pub work_directory: WorkDirectory,
|
pub work_directory: WorkDirectory,
|
||||||
pub(crate) branch: Option<Arc<str>>,
|
pub(crate) branch: Option<Arc<str>>,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue