mirror of
https://github.com/orhun/git-cliff.git
synced 2025-01-07 10:48:03 +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})"}`
|
- `{ 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.
|
- 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
|
#### 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::{
|
use crate::config::{
|
||||||
CommitParser,
|
CommitParser,
|
||||||
CommitPreprocessor,
|
CommitPreprocessor,
|
||||||
@ -75,7 +76,7 @@ impl Commit<'_> {
|
|||||||
pub fn process(&self, config: &GitConfig) -> Result<Self> {
|
pub fn process(&self, config: &GitConfig) -> Result<Self> {
|
||||||
let mut commit = self.clone();
|
let mut commit = self.clone();
|
||||||
if let Some(preprocessors) = &config.commit_preprocessors {
|
if let Some(preprocessors) = &config.commit_preprocessors {
|
||||||
commit = commit.preprocess(preprocessors);
|
commit = commit.preprocess(preprocessors)?;
|
||||||
}
|
}
|
||||||
if config.conventional_commits.unwrap_or(true) {
|
if config.conventional_commits.unwrap_or(true) {
|
||||||
if config.filter_unconventional.unwrap_or(true) {
|
if config.filter_unconventional.unwrap_or(true) {
|
||||||
@ -109,17 +110,31 @@ impl Commit<'_> {
|
|||||||
|
|
||||||
/// Preprocesses the commit using [`CommitPreprocessor`]s.
|
/// 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
|
/// [`message`]: Commit::message
|
||||||
pub fn preprocess(mut self, preprocessors: &[CommitPreprocessor]) -> Self {
|
pub fn preprocess(
|
||||||
preprocessors.iter().for_each(|preprocessor| {
|
mut self,
|
||||||
self.message = preprocessor
|
preprocessors: &[CommitPreprocessor],
|
||||||
.pattern
|
) -> Result<Self> {
|
||||||
.replace_all(&self.message, &preprocessor.replace)
|
preprocessors.iter().try_for_each(|preprocessor| {
|
||||||
.to_string();
|
if let Some(text) = &preprocessor.replace {
|
||||||
});
|
self.message = preprocessor
|
||||||
self
|
.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.
|
/// Parses the commit using [`CommitParser`]s.
|
||||||
|
@ -86,9 +86,11 @@ pub struct CommitParser {
|
|||||||
pub struct CommitPreprocessor {
|
pub struct CommitPreprocessor {
|
||||||
/// Regex for matching a text to replace.
|
/// Regex for matching a text to replace.
|
||||||
#[serde(with = "serde_regex")]
|
#[serde(with = "serde_regex")]
|
||||||
pub pattern: Regex,
|
pub pattern: Regex,
|
||||||
/// Replacement text.
|
/// 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.
|
/// Parser for extracting links in commits.
|
||||||
|
@ -6,6 +6,8 @@ pub use glob;
|
|||||||
/// Export `regex` crate.
|
/// Export `regex` crate.
|
||||||
pub use regex;
|
pub use regex;
|
||||||
|
|
||||||
|
/// Command runner.
|
||||||
|
pub mod command;
|
||||||
/// Git commit.
|
/// Git commit.
|
||||||
pub mod commit;
|
pub mod commit;
|
||||||
/// Config file parser.
|
/// Config file parser.
|
||||||
|
@ -41,8 +41,9 @@ fn generate_changelog() -> Result<()> {
|
|||||||
conventional_commits: Some(true),
|
conventional_commits: Some(true),
|
||||||
filter_unconventional: Some(true),
|
filter_unconventional: Some(true),
|
||||||
commit_preprocessors: Some(vec![CommitPreprocessor {
|
commit_preprocessors: Some(vec![CommitPreprocessor {
|
||||||
pattern: Regex::new(r#"\(fixes (#[1-9]+)\)"#).unwrap(),
|
pattern: Regex::new(r#"\(fixes (#[1-9]+)\)"#).unwrap(),
|
||||||
replace: String::from("[closes Issue${1}]"),
|
replace: Some(String::from("[closes Issue${1}]")),
|
||||||
|
replace_command: None,
|
||||||
}]),
|
}]),
|
||||||
commit_parsers: Some(vec![
|
commit_parsers: Some(vec![
|
||||||
CommitParser {
|
CommitParser {
|
||||||
|
@ -192,8 +192,11 @@ mod test {
|
|||||||
conventional_commits: Some(true),
|
conventional_commits: Some(true),
|
||||||
filter_unconventional: Some(false),
|
filter_unconventional: Some(false),
|
||||||
commit_preprocessors: Some(vec![CommitPreprocessor {
|
commit_preprocessors: Some(vec![CommitPreprocessor {
|
||||||
pattern: Regex::new("<preprocess>").unwrap(),
|
pattern: Regex::new("<preprocess>").unwrap(),
|
||||||
replace: String::from("this commit is preprocessed"),
|
replace: Some(String::from(
|
||||||
|
"this commit is preprocessed",
|
||||||
|
)),
|
||||||
|
replace_command: None,
|
||||||
}]),
|
}]),
|
||||||
commit_parsers: Some(vec![
|
commit_parsers: Some(vec![
|
||||||
CommitParser {
|
CommitParser {
|
||||||
|
Loading…
Reference in New Issue
Block a user