Detailed changes
@@ -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<Option<GitCommitTemplate>>> {
+ async { Ok(None) }.boxed()
+ }
+
fn load_blob_content(&self, oid: git::Oid) -> BoxFuture<'_, Result<String>> {
self.with_state_async(false, move |state| {
state.oids.get(&oid).cloned().context("oid does not exist")
@@ -983,6 +983,8 @@ pub trait GitRepository: Send + Sync {
target_checkpoint: GitRepositoryCheckpoint,
) -> BoxFuture<'_, Result<String>>;
+ fn load_commit_template(&self) -> BoxFuture<'_, Result<Option<GitCommitTemplate>>>;
+
fn default_branch(
&self,
include_remote_name: bool,
@@ -1146,6 +1148,11 @@ pub struct GitCommitter {
pub email: Option<String>,
}
+#[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<Option<GitCommitTemplate>>> {
+ 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,
@@ -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<GitCommitter>,
local_committer_task: Option<Task<()>>,
+ commit_template: Option<GitCommitTemplate>,
bulk_staging: Option<BulkStaging>,
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<Self>,
+ ) -> Task<anyhow::Result<Option<GitCommitTemplate>>> {
+ 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
}
@@ -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<Result<Option<GitCommitTemplate>>> {
+ 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<Result<String>> {
let repository_id = self.snapshot.id;
let rx = self.send_job(None, move |state, _| async move {