diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 8498652d5ee62557f4a7a9519fce34b0b2ccc15b..ca796c1d8376e6c6b03a63eb473723fb84ebfab1 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -4,6 +4,7 @@ use crate::{FakeFs, FakeFsEntry, Fs, RemoveOptions, RenameOptions}; use anyhow::{Context as _, Result, bail}; use collections::{HashMap, HashSet}; use futures::future::{self, BoxFuture, join_all}; +use git::repository::GitCommitTemplate; use git::{ Oid, RunHook, blame::Blame, @@ -171,6 +172,10 @@ impl GitRepository for FakeGitRepository { self.executor.spawn(async move { fut.await.ok() }).boxed() } + fn load_commit_template(&self) -> BoxFuture<'_, Result>> { + async { Ok(None) }.boxed() + } + fn load_blob_content(&self, oid: git::Oid) -> BoxFuture<'_, Result> { self.with_state_async(false, move |state| { state.oids.get(&oid).cloned().context("oid does not exist") diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 1e0f97bf6acf48a64b8464e521d1c19c421561ff..3056d91007694e35303f250e7d83d130fd2e9a38 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -983,6 +983,8 @@ pub trait GitRepository: Send + Sync { target_checkpoint: GitRepositoryCheckpoint, ) -> BoxFuture<'_, Result>; + fn load_commit_template(&self) -> BoxFuture<'_, Result>>; + fn default_branch( &self, include_remote_name: bool, @@ -1146,6 +1148,11 @@ pub struct GitCommitter { pub email: Option, } +#[derive(Clone, Debug)] +pub struct GitCommitTemplate { + pub template: String, +} + pub async fn get_git_committer(cx: &AsyncApp) -> GitCommitter { if cfg!(any(feature = "test-support", test)) { return GitCommitter { @@ -1501,6 +1508,58 @@ impl GitRepository for RealGitRepository { .boxed() } + fn load_commit_template(&self) -> BoxFuture<'_, Result>> { + let working_directory_and_git_binary = self.working_directory().map(|working_directory| { + ( + working_directory.clone(), + GitBinary::new( + self.any_git_binary_path.clone(), + working_directory, + self.path(), + self.executor.clone(), + self.is_trusted(), + ), + ) + }); + + self.executor + .spawn(async move { + let (working_directory, git_binary) = working_directory_and_git_binary?; + + let output = git_binary + .build_command(&["config", "--get", "commit.template"]) + .output() + .await + .context("failed to run git config --get commit.template")?; + + let raw_path = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if !output.status.success() || raw_path.is_empty() { + return Ok(None); + } + + let path = PathBuf::from(&raw_path); + let path = if let Some(path) = raw_path.strip_prefix("~/") { + paths::home_dir().join(path) + } else if path.is_relative() { + working_directory.join(path) + } else { + path + }; + + let template = match std::fs::read_to_string(&path) { + Ok(s) if !s.trim().is_empty() => Some(s), + Err(err) => { + log::warn!("failed to read commit template {}: {}", path.display(), err); + None + } + _ => None, + }; + + Ok(template.map(|template| GitCommitTemplate { template })) + }) + .boxed() + } + fn set_index_text( &self, path: RepoPath, diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 36bcd30d94a0450bc6d0927e5541b32b0b9ff73f..800469aedf18252fd29952500ca067641708f936 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -25,8 +25,8 @@ use file_icons::FileIcons; use futures::StreamExt as _; use git::commit::ParsedCommitMessage; use git::repository::{ - Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, GitCommitter, - PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, + Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, GitCommitTemplate, + GitCommitter, PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus, get_git_committer, }; use git::stash::GitStash; @@ -652,6 +652,7 @@ pub struct GitPanel { show_placeholders: bool, local_committer: Option, local_committer_task: Option>, + commit_template: Option, bulk_staging: Option, stash_entries: GitStash, @@ -835,6 +836,7 @@ impl GitPanel { show_placeholders: false, local_committer: None, local_committer_task: None, + commit_template: None, context_menu: None, workspace: workspace.weak_handle(), modal_open: false, @@ -3483,10 +3485,29 @@ impl GitPanel { cx, ) }); + let load_template = self.load_commit_template(cx); cx.spawn_in(window, async move |git_panel, cx| { let buffer = load_buffer.await?; + let template = load_template.await?; + git_panel.update_in(cx, |git_panel, window, cx| { + git_panel.commit_template = template; + if buffer.read(cx).text().trim().is_empty() { + let template_text = git_panel + .commit_template + .as_ref() + .map(|t| t.template.clone()) + .unwrap_or_default(); + if !template_text.is_empty() { + buffer.update(cx, |buffer, cx| { + let start = buffer.anchor_before(0); + let end = buffer.anchor_after(buffer.len()); + buffer.edit([(start..end, template_text)], None, cx); + }); + } + } + if git_panel .commit_editor .read(cx) @@ -5508,6 +5529,19 @@ impl GitPanel { !self.project.read(cx).is_read_only(cx) } + pub fn load_commit_template( + &self, + cx: &mut Context, + ) -> Task>> { + let Some(repo) = self.active_repository.clone() else { + return Task::ready(Err(anyhow::anyhow!("no active repo"))); + }; + repo.update(cx, |repo, cx| { + let rx = repo.load_commit_template_text(); + cx.spawn(async move |_, _| rx.await?) + }) + } + pub fn amend_pending(&self) -> bool { self.amend_pending } diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 592fc8daf48d523a1f2805dede6f07013f65082a..32bbf710bc6a18274ce533dcdb123bc041d2bdcb 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -33,9 +33,10 @@ use git::{ parse_git_remote_url, repository::{ Branch, CommitDetails, CommitDiff, CommitFile, CommitOptions, CreateWorktreeTarget, - DiffType, FetchOptions, GitRepository, GitRepositoryCheckpoint, GraphCommitData, - InitialGraphCommitData, LogOrder, LogSource, PushOptions, Remote, RemoteCommandOutput, - RepoPath, ResetMode, SearchCommitArgs, UpstreamTrackingStatus, Worktree as GitWorktree, + DiffType, FetchOptions, GitCommitTemplate, GitRepository, GitRepositoryCheckpoint, + GraphCommitData, InitialGraphCommitData, LogOrder, LogSource, PushOptions, Remote, + RemoteCommandOutput, RepoPath, ResetMode, SearchCommitArgs, UpstreamTrackingStatus, + Worktree as GitWorktree, }, stash::{GitStash, StashEntry}, status::{ @@ -7060,6 +7061,19 @@ impl Repository { cx.spawn(|_: &mut AsyncApp| async move { rx.await? }) } + pub fn load_commit_template_text( + &mut self, + ) -> oneshot::Receiver>> { + self.send_job(None, move |git_repo, _cx| async move { + match git_repo { + RepositoryState::Local(LocalRepositoryState { backend, .. }) => { + backend.load_commit_template().await + } + RepositoryState::Remote(_) => Ok(None), + } + }) + } + fn load_blob_content(&mut self, oid: Oid, cx: &App) -> Task> { let repository_id = self.snapshot.id; let rx = self.send_job(None, move |state, _| async move {