mirror of
https://github.com/orhun/git-cliff.git
synced 2025-01-05 15:58:24 +03:00
feat(changelog): support external commands for commit preprocessors (#86)
This commit is contained in:
parent
8d8981c6b1
commit
7d0786ca55
13
README.md
13
README.md
@ -523,6 +523,19 @@ Examples:
|
||||
- `{ pattern = "([ \\n])(([a-f0-9]{7})[a-f0-9]*)", replace = "${1}commit # [${3}](https://github.com/orhun/git-cliff/commit/${2})"}`
|
||||
- Hyperlink bare commit hashes like "abcd1234" in commit logs, with short commit hash as description.
|
||||
|
||||
Custom OS commands can also be used for modifying the commit messages:
|
||||
|
||||
- `{ pattern = "foo", replace_command = "pandoc -t commonmark"}`
|
||||
|
||||
This is useful when you want to filter/encode messages using external commands. In the example above, [pandoc](https://pandoc.org/) is used to convert each commit message that matches the given `pattern` to the [CommonMark](https://commonmark.org/) format.
|
||||
|
||||
A more fun example would be reversing the each commit message:
|
||||
|
||||
- `{ pattern = '.*', replace_command = 'rev | xargs echo "reversed: $@"' }`
|
||||
|
||||
`$COMMIT_SHA` environment variable is set during execution of the command so you can do fancier things like reading the commit itself:
|
||||
|
||||
- `{ pattern = '.*', replace_command = 'git show -s --format=%B $COMMIT_SHA' }`
|
||||
|
||||
#### commit_parsers
|
||||
|
||||
|
81
git-cliff-core/src/command.rs
Normal file
81
git-cliff-core/src/command.rs
Normal file
@ -0,0 +1,81 @@
|
||||
use crate::error::Result;
|
||||
use std::io::{
|
||||
Error as IoError,
|
||||
ErrorKind as IoErrorKind,
|
||||
Write,
|
||||
};
|
||||
use std::process::{
|
||||
Command,
|
||||
Stdio,
|
||||
};
|
||||
use std::str;
|
||||
use std::thread;
|
||||
|
||||
/// Runs the given OS command and returns the output as string.
|
||||
///
|
||||
/// Use `input` parameter to specify a text to write to stdin.
|
||||
/// Environment variables are set accordingly to `envs`.
|
||||
pub fn run(
|
||||
command: &str,
|
||||
input: Option<String>,
|
||||
envs: Vec<(&str, &str)>,
|
||||
) -> Result<String> {
|
||||
let mut child = if cfg!(target_os = "windows") {
|
||||
Command::new("cmd")
|
||||
.args(&["/C", command])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
} else {
|
||||
Command::new("sh")
|
||||
.envs(envs)
|
||||
.args(&["-c", command])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
}?;
|
||||
if let Some(input) = input {
|
||||
let mut stdin = child.stdin.take().ok_or_else(|| {
|
||||
IoError::new(IoErrorKind::Other, "stdin is not captured")
|
||||
})?;
|
||||
thread::spawn(move || {
|
||||
stdin
|
||||
.write_all(input.as_bytes())
|
||||
.expect("Failed to write to stdin");
|
||||
});
|
||||
}
|
||||
let output = child.wait_with_output()?;
|
||||
if output.status.success() {
|
||||
Ok(str::from_utf8(&output.stdout)?.to_string())
|
||||
} else {
|
||||
Err(IoError::new(
|
||||
IoErrorKind::Other,
|
||||
format!("command exited with {:?}", output.status),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
#[test]
|
||||
#[cfg(target_family = "unix")]
|
||||
fn run_os_command() -> Result<()> {
|
||||
assert_eq!(
|
||||
"eroc-ffilc-tig",
|
||||
run("echo $APP_NAME | rev", None, vec![(
|
||||
"APP_NAME",
|
||||
env!("CARGO_PKG_NAME")
|
||||
)])?
|
||||
.trim()
|
||||
);
|
||||
assert_eq!(
|
||||
"eroc-ffilc-tig",
|
||||
run("rev", Some(env!("CARGO_PKG_NAME").to_string()), vec![])?.trim()
|
||||
);
|
||||
assert_eq!("testing", run("echo 'testing'", None, vec![])?.trim());
|
||||
assert!(run("some_command", None, vec![]).is_err());
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
use crate::command;
|
||||
use crate::config::{
|
||||
CommitParser,
|
||||
CommitPreprocessor,
|
||||
@ -75,7 +76,7 @@ impl Commit<'_> {
|
||||
pub fn process(&self, config: &GitConfig) -> Result<Self> {
|
||||
let mut commit = self.clone();
|
||||
if let Some(preprocessors) = &config.commit_preprocessors {
|
||||
commit = commit.preprocess(preprocessors);
|
||||
commit = commit.preprocess(preprocessors)?;
|
||||
}
|
||||
if config.conventional_commits.unwrap_or(true) {
|
||||
if config.filter_unconventional.unwrap_or(true) {
|
||||
@ -109,17 +110,31 @@ impl Commit<'_> {
|
||||
|
||||
/// Preprocesses the commit using [`CommitPreprocessor`]s.
|
||||
///
|
||||
/// Modifies the commit [`message`] using regex.
|
||||
/// Modifies the commit [`message`] using regex or custom OS command.
|
||||
///
|
||||
/// [`message`]: Commit::message
|
||||
pub fn preprocess(mut self, preprocessors: &[CommitPreprocessor]) -> Self {
|
||||
preprocessors.iter().for_each(|preprocessor| {
|
||||
self.message = preprocessor
|
||||
.pattern
|
||||
.replace_all(&self.message, &preprocessor.replace)
|
||||
.to_string();
|
||||
});
|
||||
self
|
||||
pub fn preprocess(
|
||||
mut self,
|
||||
preprocessors: &[CommitPreprocessor],
|
||||
) -> Result<Self> {
|
||||
preprocessors.iter().try_for_each(|preprocessor| {
|
||||
if let Some(text) = &preprocessor.replace {
|
||||
self.message = preprocessor
|
||||
.pattern
|
||||
.replace_all(&self.message, text)
|
||||
.to_string();
|
||||
} else if let Some(command) = &preprocessor.replace_command {
|
||||
if preprocessor.pattern.is_match(&self.message) {
|
||||
self.message = command::run(
|
||||
command,
|
||||
Some(self.message.to_string()),
|
||||
vec![("COMMIT_SHA", &self.id)],
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok::<(), AppError>(())
|
||||
})?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Parses the commit using [`CommitParser`]s.
|
||||
|
@ -86,9 +86,11 @@ pub struct CommitParser {
|
||||
pub struct CommitPreprocessor {
|
||||
/// Regex for matching a text to replace.
|
||||
#[serde(with = "serde_regex")]
|
||||
pub pattern: Regex,
|
||||
pub pattern: Regex,
|
||||
/// Replacement text.
|
||||
pub replace: String,
|
||||
pub replace: Option<String>,
|
||||
/// Command that will be run for replacing the commit message.
|
||||
pub replace_command: Option<String>,
|
||||
}
|
||||
|
||||
/// Parser for extracting links in commits.
|
||||
|
@ -6,6 +6,8 @@ pub use glob;
|
||||
/// Export `regex` crate.
|
||||
pub use regex;
|
||||
|
||||
/// Command runner.
|
||||
pub mod command;
|
||||
/// Git commit.
|
||||
pub mod commit;
|
||||
/// Config file parser.
|
||||
|
@ -41,8 +41,9 @@ fn generate_changelog() -> Result<()> {
|
||||
conventional_commits: Some(true),
|
||||
filter_unconventional: Some(true),
|
||||
commit_preprocessors: Some(vec![CommitPreprocessor {
|
||||
pattern: Regex::new(r#"\(fixes (#[1-9]+)\)"#).unwrap(),
|
||||
replace: String::from("[closes Issue${1}]"),
|
||||
pattern: Regex::new(r#"\(fixes (#[1-9]+)\)"#).unwrap(),
|
||||
replace: Some(String::from("[closes Issue${1}]")),
|
||||
replace_command: None,
|
||||
}]),
|
||||
commit_parsers: Some(vec![
|
||||
CommitParser {
|
||||
|
@ -192,8 +192,11 @@ mod test {
|
||||
conventional_commits: Some(true),
|
||||
filter_unconventional: Some(false),
|
||||
commit_preprocessors: Some(vec![CommitPreprocessor {
|
||||
pattern: Regex::new("<preprocess>").unwrap(),
|
||||
replace: String::from("this commit is preprocessed"),
|
||||
pattern: Regex::new("<preprocess>").unwrap(),
|
||||
replace: Some(String::from(
|
||||
"this commit is preprocessed",
|
||||
)),
|
||||
replace_command: None,
|
||||
}]),
|
||||
commit_parsers: Some(vec![
|
||||
CommitParser {
|
||||
|
Loading…
Reference in New Issue
Block a user