feat(commit): 添加提交消息模板支持

- 移除 config 命令中未使用的 List 子命令及相关显示字段
- 统一 ChangelogCommand 和 CommitCommand 的 ContentGenerator 初始化方式
This commit is contained in:
2026-06-03 15:20:50 +08:00
parent 459670f363
commit 14ebb6857a
17 changed files with 494 additions and 653 deletions

View File

@@ -124,6 +124,11 @@ impl GitProfile {
.or_else(|| self.gpg.as_ref().map(|g| g.key_id.as_str()))
}
/// Get the commit template if set
pub fn commit_template(&self) -> Option<&str> {
self.settings.commit_template.as_deref()
}
/// Add a token to the profile
pub fn add_token(&mut self, service: String, token: TokenConfig) {
self.tokens.insert(service, token);
@@ -159,78 +164,120 @@ impl GitProfile {
pub fn apply_to_repo(&self, repo: &git2::Repository) -> Result<()> {
let mut config = repo.config()?;
config.set_str("user.name", &self.user_name)?;
config.set_str("user.email", &self.user_email)?;
if let Some(key) = self.signing_key() {
config.set_str("user.signingkey", key)?;
if self.settings.auto_sign_commits {
config.set_bool("commit.gpgsign", true)?;
}
if self.settings.auto_sign_tags {
config.set_bool("tag.gpgsign", true)?;
}
// Clean up old managed keys that the new profile won't set
if self.ssh.as_ref().and_then(|s| s.git_ssh_command()).is_none() {
let _ = config.remove("core.sshCommand");
}
if self.signing_key().is_none() {
let _ = config.remove("user.signingkey");
let _ = config.remove("commit.gpgsign");
let _ = config.remove("tag.gpgsign");
}
if self.gpg.is_none() {
let _ = config.remove("gpg.program");
}
if let Some(ref ssh) = self.ssh
&& let Some(ref key_path) = ssh.private_key_path
{
let path_str = key_path.display().to_string();
#[cfg(target_os = "windows")]
{
config.set_str(
"core.sshCommand",
&format!("ssh -i \"{}\"", path_str.replace('\\', "/")),
)?;
// Apply new values; track whether we've written past name/email for rollback
let mut wrote_optional = false;
let result = (|| -> Result<()> {
config.set_str("user.name", &self.user_name)?;
config.set_str("user.email", &self.user_email)?;
if let Some(ref gpg) = self.gpg {
config.set_str("gpg.program", &gpg.program)?;
wrote_optional = true;
}
#[cfg(not(target_os = "windows"))]
{
config.set_str("core.sshCommand", &format!("ssh -i '{}'", path_str))?;
if let Some(key) = self.signing_key() {
config.set_str("user.signingkey", key)?;
if self.settings.auto_sign_commits {
config.set_bool("commit.gpgsign", true)?;
}
if self.settings.auto_sign_tags {
config.set_bool("tag.gpgsign", true)?;
}
wrote_optional = true;
}
if let Some(ref ssh) = self.ssh {
if let Some(ssh_cmd) = ssh.git_ssh_command() {
config.set_str("core.sshCommand", &ssh_cmd)?;
wrote_optional = true;
}
}
Ok(())
})();
if result.is_err() && wrote_optional {
let _ = config.remove("core.sshCommand");
let _ = config.remove("user.signingkey");
let _ = config.remove("commit.gpgsign");
let _ = config.remove("tag.gpgsign");
let _ = config.remove("gpg.program");
}
Ok(())
result
}
/// Apply this profile globally
pub fn apply_global(&self) -> Result<()> {
let mut config = git2::Config::open_default()?;
config.set_str("user.name", &self.user_name)?;
config.set_str("user.email", &self.user_email)?;
if let Some(key) = self.signing_key() {
config.set_str("user.signingkey", key)?;
if self.settings.auto_sign_commits {
config.set_bool("commit.gpgsign", true)?;
}
if self.settings.auto_sign_tags {
config.set_bool("tag.gpgsign", true)?;
}
// Clean up old managed keys that the new profile won't set
if self.ssh.as_ref().and_then(|s| s.git_ssh_command()).is_none() {
let _ = config.remove("core.sshCommand");
}
if self.signing_key().is_none() {
let _ = config.remove("user.signingkey");
let _ = config.remove("commit.gpgsign");
let _ = config.remove("tag.gpgsign");
}
if self.gpg.is_none() {
let _ = config.remove("gpg.program");
}
if let Some(ref ssh) = self.ssh
&& let Some(ref key_path) = ssh.private_key_path
{
let path_str = key_path.display().to_string();
#[cfg(target_os = "windows")]
{
config.set_str(
"core.sshCommand",
&format!("ssh -i \"{}\"", path_str.replace('\\', "/")),
)?;
// Apply new values; track whether we've written past name/email for rollback
let mut wrote_optional = false;
let result = (|| -> Result<()> {
config.set_str("user.name", &self.user_name)?;
config.set_str("user.email", &self.user_email)?;
if let Some(ref gpg) = self.gpg {
config.set_str("gpg.program", &gpg.program)?;
wrote_optional = true;
}
#[cfg(not(target_os = "windows"))]
{
config.set_str("core.sshCommand", &format!("ssh -i '{}'", path_str))?;
if let Some(key) = self.signing_key() {
config.set_str("user.signingkey", key)?;
if self.settings.auto_sign_commits {
config.set_bool("commit.gpgsign", true)?;
}
if self.settings.auto_sign_tags {
config.set_bool("tag.gpgsign", true)?;
}
wrote_optional = true;
}
if let Some(ref ssh) = self.ssh {
if let Some(ssh_cmd) = ssh.git_ssh_command() {
config.set_str("core.sshCommand", &ssh_cmd)?;
wrote_optional = true;
}
}
Ok(())
})();
if result.is_err() && wrote_optional {
let _ = config.remove("core.sshCommand");
let _ = config.remove("user.signingkey");
let _ = config.remove("commit.gpgsign");
let _ = config.remove("tag.gpgsign");
let _ = config.remove("gpg.program");
}
Ok(())
result
}
/// Compare with current git configuration
@@ -349,25 +396,72 @@ impl SshConfig {
bail!("SSH public key does not exist: {:?}", path);
}
if let Some(ref path) = self.known_hosts_file
&& !path.exists()
{
bail!("SSH known_hosts file does not exist: {:?}", path);
}
Ok(())
}
/// Get SSH command for git
/// Get the effective public key path, deriving from private key if not explicitly set
pub fn effective_public_key_path(&self) -> Option<std::path::PathBuf> {
self.public_key_path.clone().or_else(|| {
self.private_key_path.as_ref().map(|pk| {
let mut pub_path = pk.clone();
let ext = pk
.extension()
.map(|e| format!("{}.pub", e.to_string_lossy()))
.unwrap_or_else(|| "pub".to_string());
pub_path.set_extension(&ext);
pub_path
})
})
}
/// Get the effective SSH command for git config
///
/// Priority: custom `ssh_command` > constructed from key/agent/known_hosts
pub fn git_ssh_command(&self) -> Option<String> {
if let Some(ref cmd) = self.ssh_command {
Some(cmd.clone())
} else if let Some(ref key_path) = self.private_key_path {
return Some(cmd.clone());
}
let mut parts: Vec<String> = vec!["ssh".to_string()];
if self.agent_forwarding {
parts.push("-A".to_string());
}
if let Some(ref key_path) = self.private_key_path {
let path_str = key_path.display().to_string();
#[cfg(target_os = "windows")]
{
Some(format!("ssh -i \"{}\"", path_str.replace('\\', "/")))
parts.push(format!("-i \"{}\"", path_str.replace('\\', "/")));
}
#[cfg(not(target_os = "windows"))]
{
Some(format!("ssh -i '{}'", path_str))
parts.push(format!("-i '{}'", path_str));
}
} else {
}
if let Some(ref known_hosts) = self.known_hosts_file {
let kh_str = known_hosts.display().to_string();
#[cfg(target_os = "windows")]
{
parts.push(format!("-o UserKnownHostsFile=\"{}\"", kh_str.replace('\\', "/")));
}
#[cfg(not(target_os = "windows"))]
{
parts.push(format!("-o UserKnownHostsFile='{}'", kh_str));
}
}
if parts.len() == 1 {
None
} else {
Some(parts.join(" "))
}
}
}