style(formatting): use spaces instead of tabs (#1184)

Update formatting rules to use spaces instead of tabs for indentation,
wrap at 100 characters, and remove the setting that aligns struct fields
vertically as this generally harms readability.

These changes make the code more consistent with the Rust community's
conventions and improve readability by not truncating lines too early.
This commit is contained in:
Josh McKinney
2025-07-27 12:16:15 +03:00
committed by GitHub
parent e721a88ac5
commit 00273009e7
30 changed files with 8177 additions and 8647 deletions
+1 -4
View File
@@ -112,10 +112,7 @@ jobs:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Check the formatting
uses: actions-rs/cargo@844f36862e911db73fe0815f00a4a2602c279505 # v1.0.3
with:
command: fmt
args: --all -- --check --verbose
run: cargo +nightly fmt --all -- --check --verbose
doctest:
name: Doctests
File diff suppressed because it is too large Load Diff
+62 -69
View File
@@ -8,78 +8,71 @@ use crate::error::Result;
///
/// 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> {
log::trace!("Running command: {:?}", command);
let mut child = if cfg!(target_os = "windows") {
Command::new("cmd")
.envs(envs)
.args(["/C", command])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.current_dir(env::current_dir()?)
.spawn()
} else {
Command::new("sh")
.envs(envs)
.args(["-c", command])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.current_dir(env::current_dir()?)
.spawn()
}?;
if let Some(input) = input {
let mut stdin = child
.stdin
.take()
.ok_or_else(|| IoError::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 {
for output in [output.stdout, output.stderr] {
let output = str::from_utf8(&output)?.to_string();
if !output.is_empty() {
log::error!("{}", output);
}
}
Err(
IoError::other(format!("command exited with {:?}", output.status))
.into(),
)
}
pub fn run(command: &str, input: Option<String>, envs: Vec<(&str, &str)>) -> Result<String> {
log::trace!("Running command: {:?}", command);
let mut child = if cfg!(target_os = "windows") {
Command::new("cmd")
.envs(envs)
.args(["/C", command])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.current_dir(env::current_dir()?)
.spawn()
} else {
Command::new("sh")
.envs(envs)
.args(["-c", command])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.current_dir(env::current_dir()?)
.spawn()
}?;
if let Some(input) = input {
let mut stdin = child
.stdin
.take()
.ok_or_else(|| IoError::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 {
for output in [output.stdout, output.stderr] {
let output = str::from_utf8(&output)?.to_string();
if !output.is_empty() {
log::error!("{}", output);
}
}
Err(IoError::other(format!("command exited with {:?}", output.status)).into())
}
}
#[cfg(test)]
mod test {
use super::*;
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(())
}
#[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(())
}
}
File diff suppressed because it is too large Load Diff
+421 -440
View File
@@ -17,556 +17,537 @@ const DEFAULT_INITIAL_TAG: &str = "0.1.0";
/// Manifest file information and regex for matching contents.
#[derive(Debug)]
struct ManifestInfo {
/// Path of the manifest.
path: PathBuf,
/// Regular expression for matching metadata in the manifest.
regex: Regex,
/// Path of the manifest.
path: PathBuf,
/// Regular expression for matching metadata in the manifest.
regex: Regex,
}
lazy_static::lazy_static! {
/// Array containing manifest information for Rust and Python projects.
static ref MANIFEST_INFO: Vec<ManifestInfo> = vec![
ManifestInfo {
path: PathBuf::from("Cargo.toml"),
regex: RegexBuilder::new(
r"^\[(?:workspace|package)\.metadata\.git\-cliff\.",
)
.multi_line(true)
.build()
.expect("failed to build regex"),
},
ManifestInfo {
path: PathBuf::from("pyproject.toml"),
regex: RegexBuilder::new(r"^\[(?:tool)\.git\-cliff\.")
.multi_line(true)
.build()
.expect("failed to build regex"),
},
];
/// Array containing manifest information for Rust and Python projects.
static ref MANIFEST_INFO: Vec<ManifestInfo> = vec![
ManifestInfo {
path: PathBuf::from("Cargo.toml"),
regex: RegexBuilder::new(
r"^\[(?:workspace|package)\.metadata\.git\-cliff\.",
)
.multi_line(true)
.build()
.expect("failed to build regex"),
},
ManifestInfo {
path: PathBuf::from("pyproject.toml"),
regex: RegexBuilder::new(r"^\[(?:tool)\.git\-cliff\.")
.multi_line(true)
.build()
.expect("failed to build regex"),
},
];
}
/// Configuration values.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
/// Configuration values about changelog generation.
#[serde(default)]
pub changelog: ChangelogConfig,
/// Configuration values about git.
#[serde(default)]
pub git: GitConfig,
/// Configuration values about remote.
#[serde(default)]
pub remote: RemoteConfig,
/// Configuration values about bump version.
#[serde(default)]
pub bump: Bump,
/// Configuration values about changelog generation.
#[serde(default)]
pub changelog: ChangelogConfig,
/// Configuration values about git.
#[serde(default)]
pub git: GitConfig,
/// Configuration values about remote.
#[serde(default)]
pub remote: RemoteConfig,
/// Configuration values about bump version.
#[serde(default)]
pub bump: Bump,
}
/// Changelog configuration.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct ChangelogConfig {
/// Changelog header.
pub header: Option<String>,
/// Changelog body, template.
pub body: String,
/// Changelog footer.
pub footer: Option<String>,
/// Trim the template.
pub trim: bool,
/// Always render the body template.
pub render_always: bool,
/// Changelog postprocessors.
pub postprocessors: Vec<TextProcessor>,
/// Output file path.
pub output: Option<PathBuf>,
/// Changelog header.
pub header: Option<String>,
/// Changelog body, template.
pub body: String,
/// Changelog footer.
pub footer: Option<String>,
/// Trim the template.
pub trim: bool,
/// Always render the body template.
pub render_always: bool,
/// Changelog postprocessors.
pub postprocessors: Vec<TextProcessor>,
/// Output file path.
pub output: Option<PathBuf>,
}
/// Git configuration
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct GitConfig {
/// Parse commits according to the conventional commits specification.
pub conventional_commits: bool,
/// Require all commits to be conventional.
/// Takes precedence over filter_unconventional.
pub require_conventional: bool,
/// Exclude commits that do not match the conventional commits specification
/// from the changelog.
pub filter_unconventional: bool,
/// Split commits on newlines, treating each line as an individual commit.
pub split_commits: bool,
/// Parse commits according to the conventional commits specification.
pub conventional_commits: bool,
/// Require all commits to be conventional.
/// Takes precedence over filter_unconventional.
pub require_conventional: bool,
/// Exclude commits that do not match the conventional commits specification
/// from the changelog.
pub filter_unconventional: bool,
/// Split commits on newlines, treating each line as an individual commit.
pub split_commits: bool,
/// An array of regex based parsers to modify commit messages prior to
/// further processing.
pub commit_preprocessors: Vec<TextProcessor>,
/// An array of regex based parsers for extracting data from the commit
/// message.
pub commit_parsers: Vec<CommitParser>,
/// Prevent commits having the `BREAKING CHANGE:` footer from being excluded
/// by commit parsers.
pub protect_breaking_commits: bool,
/// An array of regex based parsers to extract links from the commit message
/// and add them to the commit's context.
pub link_parsers: Vec<LinkParser>,
/// Exclude commits that are not matched by any commit parser.
pub filter_commits: bool,
/// Regex to select git tags that represent releases.
#[serde(with = "serde_regex", default)]
pub tag_pattern: Option<Regex>,
/// Regex to select git tags that do not represent proper releases.
#[serde(with = "serde_regex", default)]
pub skip_tags: Option<Regex>,
/// Regex to exclude git tags after applying the tag_pattern.
#[serde(with = "serde_regex", default)]
pub ignore_tags: Option<Regex>,
/// Regex to count matched tags.
#[serde(with = "serde_regex", default)]
pub count_tags: Option<Regex>,
/// Include only the tags that belong to the current branch.
pub use_branch_tags: bool,
/// Order releases topologically instead of chronologically.
pub topo_order: bool,
/// Order commits chronologically instead of topologically.
pub topo_order_commits: bool,
/// How to order commits in each group/release within the changelog.
pub sort_commits: String,
/// Limit the total number of commits included in the changelog.
pub limit_commits: Option<usize>,
/// Read submodule commits.
pub recurse_submodules: Option<bool>,
/// Include related commits with changes at the specified paths.
#[serde(with = "serde_pattern", default)]
pub include_paths: Vec<Pattern>,
/// Exclude unrelated commits with changes at the specified paths.
#[serde(with = "serde_pattern", default)]
pub exclude_paths: Vec<Pattern>,
/// An array of regex based parsers to modify commit messages prior to
/// further processing.
pub commit_preprocessors: Vec<TextProcessor>,
/// An array of regex based parsers for extracting data from the commit
/// message.
pub commit_parsers: Vec<CommitParser>,
/// Prevent commits having the `BREAKING CHANGE:` footer from being excluded
/// by commit parsers.
pub protect_breaking_commits: bool,
/// An array of regex based parsers to extract links from the commit message
/// and add them to the commit's context.
pub link_parsers: Vec<LinkParser>,
/// Exclude commits that are not matched by any commit parser.
pub filter_commits: bool,
/// Regex to select git tags that represent releases.
#[serde(with = "serde_regex", default)]
pub tag_pattern: Option<Regex>,
/// Regex to select git tags that do not represent proper releases.
#[serde(with = "serde_regex", default)]
pub skip_tags: Option<Regex>,
/// Regex to exclude git tags after applying the tag_pattern.
#[serde(with = "serde_regex", default)]
pub ignore_tags: Option<Regex>,
/// Regex to count matched tags.
#[serde(with = "serde_regex", default)]
pub count_tags: Option<Regex>,
/// Include only the tags that belong to the current branch.
pub use_branch_tags: bool,
/// Order releases topologically instead of chronologically.
pub topo_order: bool,
/// Order commits chronologically instead of topologically.
pub topo_order_commits: bool,
/// How to order commits in each group/release within the changelog.
pub sort_commits: String,
/// Limit the total number of commits included in the changelog.
pub limit_commits: Option<usize>,
/// Read submodule commits.
pub recurse_submodules: Option<bool>,
/// Include related commits with changes at the specified paths.
#[serde(with = "serde_pattern", default)]
pub include_paths: Vec<Pattern>,
/// Exclude unrelated commits with changes at the specified paths.
#[serde(with = "serde_pattern", default)]
pub exclude_paths: Vec<Pattern>,
}
/// Serialize and deserialize implementation for [`glob::Pattern`].
mod serde_pattern {
use glob::Pattern;
use serde::Deserialize;
use serde::de::Error;
use serde::ser::SerializeSeq;
use glob::Pattern;
use serde::Deserialize;
use serde::de::Error;
use serde::ser::SerializeSeq;
pub fn serialize<S>(
patterns: &[Pattern],
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut seq = serializer.serialize_seq(Some(patterns.len()))?;
for pattern in patterns {
seq.serialize_element(pattern.as_str())?;
}
seq.end()
}
pub fn serialize<S>(patterns: &[Pattern], serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut seq = serializer.serialize_seq(Some(patterns.len()))?;
for pattern in patterns {
seq.serialize_element(pattern.as_str())?;
}
seq.end()
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Pattern>, D::Error>
where
D: serde::Deserializer<'de>,
{
let patterns = Vec::<String>::deserialize(deserializer)?;
patterns
.into_iter()
.map(|pattern| pattern.parse().map_err(D::Error::custom))
.collect()
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Pattern>, D::Error>
where
D: serde::Deserializer<'de>,
{
let patterns = Vec::<String>::deserialize(deserializer)?;
patterns
.into_iter()
.map(|pattern| pattern.parse().map_err(D::Error::custom))
.collect()
}
}
/// Remote configuration.
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct RemoteConfig {
/// GitHub remote.
#[serde(default)]
pub github: Remote,
/// GitLab remote.
#[serde(default)]
pub gitlab: Remote,
/// Gitea remote.
#[serde(default)]
pub gitea: Remote,
/// Bitbucket remote.
#[serde(default)]
pub bitbucket: Remote,
/// GitHub remote.
#[serde(default)]
pub github: Remote,
/// GitLab remote.
#[serde(default)]
pub gitlab: Remote,
/// Gitea remote.
#[serde(default)]
pub gitea: Remote,
/// Bitbucket remote.
#[serde(default)]
pub bitbucket: Remote,
}
impl RemoteConfig {
/// Returns `true` if any remote is set.
pub fn is_any_set(&self) -> bool {
#[cfg(feature = "github")]
if self.github.is_set() {
return true;
}
#[cfg(feature = "gitlab")]
if self.gitlab.is_set() {
return true;
}
#[cfg(feature = "gitea")]
if self.gitea.is_set() {
return true;
}
#[cfg(feature = "bitbucket")]
if self.bitbucket.is_set() {
return true;
}
false
}
/// Returns `true` if any remote is set.
pub fn is_any_set(&self) -> bool {
#[cfg(feature = "github")]
if self.github.is_set() {
return true;
}
#[cfg(feature = "gitlab")]
if self.gitlab.is_set() {
return true;
}
#[cfg(feature = "gitea")]
if self.gitea.is_set() {
return true;
}
#[cfg(feature = "bitbucket")]
if self.bitbucket.is_set() {
return true;
}
false
}
/// Enables the native TLS for all remotes.
pub fn enable_native_tls(&mut self) {
#[cfg(feature = "github")]
{
self.github.native_tls = Some(true);
}
#[cfg(feature = "gitlab")]
{
self.gitlab.native_tls = Some(true);
}
#[cfg(feature = "gitea")]
{
self.gitea.native_tls = Some(true);
}
#[cfg(feature = "bitbucket")]
{
self.bitbucket.native_tls = Some(true);
}
}
/// Enables the native TLS for all remotes.
pub fn enable_native_tls(&mut self) {
#[cfg(feature = "github")]
{
self.github.native_tls = Some(true);
}
#[cfg(feature = "gitlab")]
{
self.gitlab.native_tls = Some(true);
}
#[cfg(feature = "gitea")]
{
self.gitea.native_tls = Some(true);
}
#[cfg(feature = "bitbucket")]
{
self.bitbucket.native_tls = Some(true);
}
}
}
/// A single remote.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Remote {
/// Owner of the remote.
pub owner: String,
/// Repository name.
pub repo: String,
/// Access token.
#[serde(skip_serializing)]
pub token: Option<SecretString>,
/// Whether if the remote is set manually.
#[serde(skip_deserializing, default = "default_true")]
pub is_custom: bool,
/// Remote API URL.
pub api_url: Option<String>,
/// Whether to use native TLS.
pub native_tls: Option<bool>,
/// Owner of the remote.
pub owner: String,
/// Repository name.
pub repo: String,
/// Access token.
#[serde(skip_serializing)]
pub token: Option<SecretString>,
/// Whether if the remote is set manually.
#[serde(skip_deserializing, default = "default_true")]
pub is_custom: bool,
/// Remote API URL.
pub api_url: Option<String>,
/// Whether to use native TLS.
pub native_tls: Option<bool>,
}
/// Returns `true` for serde's `default` attribute.
fn default_true() -> bool {
true
true
}
impl fmt::Display for Remote {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.owner, self.repo)
}
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.owner, self.repo)
}
}
impl PartialEq for Remote {
fn eq(&self, other: &Self) -> bool {
self.to_string() == other.to_string()
}
fn eq(&self, other: &Self) -> bool {
self.to_string() == other.to_string()
}
}
impl Remote {
/// Constructs a new instance.
pub fn new<S: Into<String>>(owner: S, repo: S) -> Self {
Self {
owner: owner.into(),
repo: repo.into(),
token: None,
is_custom: false,
api_url: None,
native_tls: None,
}
}
/// Constructs a new instance.
pub fn new<S: Into<String>>(owner: S, repo: S) -> Self {
Self {
owner: owner.into(),
repo: repo.into(),
token: None,
is_custom: false,
api_url: None,
native_tls: None,
}
}
/// Returns `true` if the remote has an owner and repo.
pub fn is_set(&self) -> bool {
!self.owner.is_empty() && !self.repo.is_empty()
}
/// Returns `true` if the remote has an owner and repo.
pub fn is_set(&self) -> bool {
!self.owner.is_empty() && !self.repo.is_empty()
}
}
/// Version bump type.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum BumpType {
/// Bump major version.
Major,
/// Bump minor version.
Minor,
/// Bump patch version.
Patch,
/// Bump major version.
Major,
/// Bump minor version.
Minor,
/// Bump patch version.
Patch,
}
/// Bump version configuration.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Bump {
/// Configures automatic minor version increments for feature changes.
///
/// When `true`, a feature will always trigger a minor version update.
/// When `false`, a feature will trigger:
///
/// - A patch version update if the major version is 0.
/// - A minor version update otherwise.
pub features_always_bump_minor: Option<bool>,
/// Configures automatic minor version increments for feature changes.
///
/// When `true`, a feature will always trigger a minor version update.
/// When `false`, a feature will trigger:
///
/// - A patch version update if the major version is 0.
/// - A minor version update otherwise.
pub features_always_bump_minor: Option<bool>,
/// Configures 0 -> 1 major version increments for breaking changes.
///
/// When `true`, a breaking change commit will always trigger a major
/// version update (including the transition from version 0 to 1)
/// When `false`, a breaking change commit will trigger:
///
/// - A minor version update if the major version is 0.
/// - A major version update otherwise.
pub breaking_always_bump_major: Option<bool>,
/// Configures 0 -> 1 major version increments for breaking changes.
///
/// When `true`, a breaking change commit will always trigger a major
/// version update (including the transition from version 0 to 1)
/// When `false`, a breaking change commit will trigger:
///
/// - A minor version update if the major version is 0.
/// - A major version update otherwise.
pub breaking_always_bump_major: Option<bool>,
/// Configures the initial version of the project.
///
/// When set, the version will be set to this value if no tags are found.
pub initial_tag: Option<String>,
/// Configures the initial version of the project.
///
/// When set, the version will be set to this value if no tags are found.
pub initial_tag: Option<String>,
/// Configure a custom regex pattern for major version increments.
///
/// This will check only the type of the commit against the given pattern.
///
/// ### Note
///
/// `commit type` according to the spec is only `[a-zA-Z]+`
pub custom_major_increment_regex: Option<String>,
/// Configure a custom regex pattern for major version increments.
///
/// This will check only the type of the commit against the given pattern.
///
/// ### Note
///
/// `commit type` according to the spec is only `[a-zA-Z]+`
pub custom_major_increment_regex: Option<String>,
/// Configure a custom regex pattern for minor version increments.
///
/// This will check only the type of the commit against the given pattern.
///
/// ### Note
///
/// `commit type` according to the spec is only `[a-zA-Z]+`
pub custom_minor_increment_regex: Option<String>,
/// Configure a custom regex pattern for minor version increments.
///
/// This will check only the type of the commit against the given pattern.
///
/// ### Note
///
/// `commit type` according to the spec is only `[a-zA-Z]+`
pub custom_minor_increment_regex: Option<String>,
/// Force to always bump in major, minor or patch.
pub bump_type: Option<BumpType>,
/// Force to always bump in major, minor or patch.
pub bump_type: Option<BumpType>,
}
impl Bump {
/// Returns the initial tag.
///
/// This function also logs the returned value.
pub fn get_initial_tag(&self) -> String {
if let Some(tag) = self.initial_tag.clone() {
warn!(
"No releases found, using initial tag '{tag}' as the next version."
);
tag
} else {
warn!(
"No releases found, using {DEFAULT_INITIAL_TAG} as the next \
version."
);
DEFAULT_INITIAL_TAG.into()
}
}
/// Returns the initial tag.
///
/// This function also logs the returned value.
pub fn get_initial_tag(&self) -> String {
if let Some(tag) = self.initial_tag.clone() {
warn!("No releases found, using initial tag '{tag}' as the next version.");
tag
} else {
warn!("No releases found, using {DEFAULT_INITIAL_TAG} as the next version.");
DEFAULT_INITIAL_TAG.into()
}
}
}
/// Parser for grouping commits.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct CommitParser {
/// SHA1 of the commit.
pub sha: Option<String>,
/// Regex for matching the commit message.
#[serde(with = "serde_regex", default)]
pub message: Option<Regex>,
/// Regex for matching the commit body.
#[serde(with = "serde_regex", default)]
pub body: Option<Regex>,
/// Regex for matching the commit footer.
#[serde(with = "serde_regex", default)]
pub footer: Option<Regex>,
/// Group of the commit.
pub group: Option<String>,
/// Default scope of the commit.
pub default_scope: Option<String>,
/// Commit scope for overriding the default scope.
pub scope: Option<String>,
/// Whether to skip this commit group.
pub skip: Option<bool>,
/// Field name of the commit to match the regex against.
pub field: Option<String>,
/// Regex for matching the field value.
#[serde(with = "serde_regex", default)]
pub pattern: Option<Regex>,
/// SHA1 of the commit.
pub sha: Option<String>,
/// Regex for matching the commit message.
#[serde(with = "serde_regex", default)]
pub message: Option<Regex>,
/// Regex for matching the commit body.
#[serde(with = "serde_regex", default)]
pub body: Option<Regex>,
/// Regex for matching the commit footer.
#[serde(with = "serde_regex", default)]
pub footer: Option<Regex>,
/// Group of the commit.
pub group: Option<String>,
/// Default scope of the commit.
pub default_scope: Option<String>,
/// Commit scope for overriding the default scope.
pub scope: Option<String>,
/// Whether to skip this commit group.
pub skip: Option<bool>,
/// Field name of the commit to match the regex against.
pub field: Option<String>,
/// Regex for matching the field value.
#[serde(with = "serde_regex", default)]
pub pattern: Option<Regex>,
}
/// `TextProcessor`, e.g. for modifying commit messages.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextProcessor {
/// Regex for matching a text to replace.
#[serde(with = "serde_regex")]
pub pattern: Regex,
/// Replacement text.
pub replace: Option<String>,
/// Command that will be run for replacing the commit message.
pub replace_command: Option<String>,
/// Regex for matching a text to replace.
#[serde(with = "serde_regex")]
pub pattern: Regex,
/// Replacement text.
pub replace: Option<String>,
/// Command that will be run for replacing the commit message.
pub replace_command: Option<String>,
}
impl TextProcessor {
/// Replaces the text with using the given pattern or the command output.
pub fn replace(
&self,
rendered: &mut String,
command_envs: Vec<(&str, &str)>,
) -> Result<()> {
if let Some(text) = &self.replace {
*rendered = self.pattern.replace_all(rendered, text).to_string();
} else if let Some(command) = &self.replace_command {
if self.pattern.is_match(rendered) {
*rendered =
command::run(command, Some(rendered.to_string()), command_envs)?;
}
}
Ok(())
}
/// Replaces the text with using the given pattern or the command output.
pub fn replace(&self, rendered: &mut String, command_envs: Vec<(&str, &str)>) -> Result<()> {
if let Some(text) = &self.replace {
*rendered = self.pattern.replace_all(rendered, text).to_string();
} else if let Some(command) = &self.replace_command {
if self.pattern.is_match(rendered) {
*rendered = command::run(command, Some(rendered.to_string()), command_envs)?;
}
}
Ok(())
}
}
/// Parser for extracting links in commits.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LinkParser {
/// Regex for finding links in the commit message.
#[serde(with = "serde_regex")]
pub pattern: Regex,
/// The string used to generate the link URL.
pub href: String,
/// The string used to generate the link text.
pub text: Option<String>,
/// Regex for finding links in the commit message.
#[serde(with = "serde_regex")]
pub pattern: Regex,
/// The string used to generate the link URL.
pub href: String,
/// The string used to generate the link text.
pub text: Option<String>,
}
impl Config {
/// Reads the config file contents from project manifest (e.g. Cargo.toml,
/// pyproject.toml)
pub fn read_from_manifest() -> Result<Option<String>> {
for info in &(*MANIFEST_INFO) {
if info.path.exists() {
let contents = fs::read_to_string(&info.path)?;
if info.regex.is_match(&contents) {
return Ok(Some(
info.regex.replace_all(&contents, "[").to_string(),
));
}
}
}
Ok(None)
}
/// Reads the config file contents from project manifest (e.g. Cargo.toml,
/// pyproject.toml)
pub fn read_from_manifest() -> Result<Option<String>> {
for info in &(*MANIFEST_INFO) {
if info.path.exists() {
let contents = fs::read_to_string(&info.path)?;
if info.regex.is_match(&contents) {
return Ok(Some(info.regex.replace_all(&contents, "[").to_string()));
}
}
}
Ok(None)
}
/// Parses the config file and returns the values.
pub fn load(path: &Path) -> Result<Config> {
if MANIFEST_INFO
.iter()
.any(|v| path.file_name() == v.path.file_name())
{
if let Some(contents) = Self::read_from_manifest()? {
return contents.parse();
}
}
/// Parses the config file and returns the values.
pub fn load(path: &Path) -> Result<Config> {
if MANIFEST_INFO
.iter()
.any(|v| path.file_name() == v.path.file_name())
{
if let Some(contents) = Self::read_from_manifest()? {
return contents.parse();
}
}
// Adding sources one after another overwrites the previous values.
// Thus adding the default config initializes the config with default values.
let default_config_str = EmbeddedConfig::get_config()?;
Ok(config::Config::builder()
.add_source(config::File::from_str(
&default_config_str,
config::FileFormat::Toml,
))
.add_source(config::File::from(path))
.add_source(
config::Environment::with_prefix("GIT_CLIFF").separator("__"),
)
.build()?
.try_deserialize()?)
}
// Adding sources one after another overwrites the previous values.
// Thus adding the default config initializes the config with default values.
let default_config_str = EmbeddedConfig::get_config()?;
Ok(config::Config::builder()
.add_source(config::File::from_str(
&default_config_str,
config::FileFormat::Toml,
))
.add_source(config::File::from(path))
.add_source(config::Environment::with_prefix("GIT_CLIFF").separator("__"))
.build()?
.try_deserialize()?)
}
}
impl FromStr for Config {
type Err = error::Error;
type Err = error::Error;
/// Parses the config file from string and returns the values.
fn from_str(contents: &str) -> Result<Self> {
// Adding sources one after another overwrites the previous values.
// Thus adding the default config initializes the config with default
// values.
let default_config_str = EmbeddedConfig::get_config()?;
/// Parses the config file from string and returns the values.
fn from_str(contents: &str) -> Result<Self> {
// Adding sources one after another overwrites the previous values.
// Thus adding the default config initializes the config with default
// values.
let default_config_str = EmbeddedConfig::get_config()?;
Ok(config::Config::builder()
.add_source(config::File::from_str(
&default_config_str,
config::FileFormat::Toml,
))
.add_source(config::File::from_str(contents, config::FileFormat::Toml))
.add_source(
config::Environment::with_prefix("GIT_CLIFF").separator("__"),
)
.build()?
.try_deserialize()?)
}
Ok(config::Config::builder()
.add_source(config::File::from_str(
&default_config_str,
config::FileFormat::Toml,
))
.add_source(config::File::from_str(contents, config::FileFormat::Toml))
.add_source(config::Environment::with_prefix("GIT_CLIFF").separator("__"))
.build()?
.try_deserialize()?)
}
}
#[cfg(test)]
mod test {
use std::env;
use std::env;
use pretty_assertions::assert_eq;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn load() -> Result<()> {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("parent directory not found")
.to_path_buf()
.join("config")
.join(crate::DEFAULT_CONFIG);
use super::*;
#[test]
fn load() -> Result<()> {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("parent directory not found")
.to_path_buf()
.join("config")
.join(crate::DEFAULT_CONFIG);
const FOOTER_VALUE: &str = "test";
const TAG_PATTERN_VALUE: &str = ".*[0-9].*";
const IGNORE_TAGS_VALUE: &str = "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+";
const FOOTER_VALUE: &str = "test";
const TAG_PATTERN_VALUE: &str = ".*[0-9].*";
const IGNORE_TAGS_VALUE: &str = "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+";
unsafe {
env::set_var("GIT_CLIFF__CHANGELOG__FOOTER", FOOTER_VALUE);
env::set_var("GIT_CLIFF__GIT__TAG_PATTERN", TAG_PATTERN_VALUE);
env::set_var("GIT_CLIFF__GIT__IGNORE_TAGS", IGNORE_TAGS_VALUE);
};
unsafe {
env::set_var("GIT_CLIFF__CHANGELOG__FOOTER", FOOTER_VALUE);
env::set_var("GIT_CLIFF__GIT__TAG_PATTERN", TAG_PATTERN_VALUE);
env::set_var("GIT_CLIFF__GIT__IGNORE_TAGS", IGNORE_TAGS_VALUE);
};
let config = Config::load(&path)?;
let config = Config::load(&path)?;
assert_eq!(Some(String::from(FOOTER_VALUE)), config.changelog.footer);
assert_eq!(
Some(String::from(TAG_PATTERN_VALUE)),
config
.git
.tag_pattern
.map(|tag_pattern| tag_pattern.to_string())
);
assert_eq!(
Some(String::from(IGNORE_TAGS_VALUE)),
config
.git
.ignore_tags
.map(|ignore_tags| ignore_tags.to_string())
);
Ok(())
}
assert_eq!(Some(String::from(FOOTER_VALUE)), config.changelog.footer);
assert_eq!(
Some(String::from(TAG_PATTERN_VALUE)),
config
.git
.tag_pattern
.map(|tag_pattern| tag_pattern.to_string())
);
assert_eq!(
Some(String::from(IGNORE_TAGS_VALUE)),
config
.git
.ignore_tags
.map(|ignore_tags| ignore_tags.to_string())
);
Ok(())
}
#[test]
fn remote_config() {
let remote1 = Remote::new("abc", "xyz1");
let remote2 = Remote::new("abc", "xyz2");
assert!(!remote1.eq(&remote2));
assert_eq!("abc/xyz1", remote1.to_string());
assert!(remote1.is_set());
assert!(!Remote::new("", "test").is_set());
assert!(!Remote::new("test", "").is_set());
assert!(!Remote::new("", "").is_set());
}
#[test]
fn remote_config() {
let remote1 = Remote::new("abc", "xyz1");
let remote2 = Remote::new("abc", "xyz2");
assert!(!remote1.eq(&remote2));
assert_eq!("abc/xyz1", remote1.to_string());
assert!(remote1.is_set());
assert!(!Remote::new("", "test").is_set());
assert!(!Remote::new("test", "").is_set());
assert!(!Remote::new("", "").is_set());
}
}
+13 -13
View File
@@ -5,20 +5,20 @@ use serde::{Deserialize, Serialize};
/// Representation of a remote contributor.
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
pub struct RemoteContributor {
/// Username.
pub username: Option<String>,
/// Title of the pull request.
pub pr_title: Option<String>,
/// The pull request that the user created.
pub pr_number: Option<i64>,
/// Labels of the pull request.
pub pr_labels: Vec<String>,
/// Whether if the user contributed for the first time.
pub is_first_time: bool,
/// Username.
pub username: Option<String>,
/// Title of the pull request.
pub pr_title: Option<String>,
/// The pull request that the user created.
pub pr_number: Option<i64>,
/// Labels of the pull request.
pub pr_labels: Vec<String>,
/// Whether if the user contributed for the first time.
pub is_first_time: bool,
}
impl Hash for RemoteContributor {
fn hash<H: Hasher>(&self, state: &mut H) {
self.username.hash(state);
}
fn hash<H: Hasher>(&self, state: &mut H) {
self.username.hash(state);
}
}
+36 -36
View File
@@ -16,22 +16,22 @@ use crate::error::{Error, Result};
pub struct EmbeddedConfig;
impl EmbeddedConfig {
/// Extracts the embedded content.
pub fn get_config() -> Result<String> {
match Self::get(crate::DEFAULT_CONFIG) {
Some(v) => Ok(str::from_utf8(&v.data)?.to_string()),
None => Err(Error::EmbeddedError(String::from(
"Embedded config not found",
))),
}
}
/// Extracts the embedded content.
pub fn get_config() -> Result<String> {
match Self::get(crate::DEFAULT_CONFIG) {
Some(v) => Ok(str::from_utf8(&v.data)?.to_string()),
None => Err(Error::EmbeddedError(String::from(
"Embedded config not found",
))),
}
}
/// Parses the extracted content into [`Config`].
///
/// [`Config`]: Config
pub fn parse() -> Result<Config> {
Self::get_config()?.parse()
}
/// Parses the extracted content into [`Config`].
///
/// [`Config`]: Config
pub fn parse() -> Result<Config> {
Self::get_config()?.parse()
}
}
/// Built-in configuration file embedder/extractor.
@@ -42,26 +42,26 @@ impl EmbeddedConfig {
pub struct BuiltinConfig;
impl BuiltinConfig {
/// Extracts the embedded content.
pub fn get_config(mut name: String) -> Result<String> {
if !Path::new(&name)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("toml"))
{
name = format!("{name}.toml");
}
let contents = match Self::get(&name) {
Some(v) => Ok(str::from_utf8(&v.data)?.to_string()),
None => Err(Error::EmbeddedError(format!("config {} not found", name,))),
}?;
Ok(contents)
}
/// Extracts the embedded content.
pub fn get_config(mut name: String) -> Result<String> {
if !Path::new(&name)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("toml"))
{
name = format!("{name}.toml");
}
let contents = match Self::get(&name) {
Some(v) => Ok(str::from_utf8(&v.data)?.to_string()),
None => Err(Error::EmbeddedError(format!("config {} not found", name,))),
}?;
Ok(contents)
}
/// Parses the extracted content into [`Config`] along with the name.
///
/// [`Config`]: Config
pub fn parse(name: String) -> Result<(Config, String)> {
let parsed = Self::get_config(name.to_string())?.parse()?;
Ok((parsed, name))
}
/// Parses the extracted content into [`Config`] along with the name.
///
/// [`Config`]: Config
pub fn parse(name: String) -> Result<(Config, String)> {
let parsed = Self::get_config(name.to_string())?.parse()?;
Ok((parsed, name))
}
}
+129 -132
View File
@@ -3,121 +3,118 @@ use thiserror::Error as ThisError;
/// Library related errors that we are exposing to the rest of the workspaces.
#[derive(Debug, ThisError)]
pub enum Error {
/// Error that may occur while I/O operations.
#[error("IO error: `{0}`")]
IoError(#[from] std::io::Error),
/// Error that may occur when attempting to interpret a sequence of u8 as a
/// string.
#[error("UTF-8 error: `{0}`")]
Utf8Error(#[from] std::str::Utf8Error),
/// Error variant that represents errors coming out of libgit2.
#[cfg(feature = "repo")]
#[error("Git error: `{0}`")]
GitError(#[from] git2::Error),
/// Error that may occur when failed to set a commit range.
#[cfg(feature = "repo")]
#[error(
"Failed to set the commit range: {1}
/// Error that may occur while I/O operations.
#[error("IO error: `{0}`")]
IoError(#[from] std::io::Error),
/// Error that may occur when attempting to interpret a sequence of u8 as a
/// string.
#[error("UTF-8 error: `{0}`")]
Utf8Error(#[from] std::str::Utf8Error),
/// Error variant that represents errors coming out of libgit2.
#[cfg(feature = "repo")]
#[error("Git error: `{0}`")]
GitError(#[from] git2::Error),
/// Error that may occur when failed to set a commit range.
#[cfg(feature = "repo")]
#[error(
"Failed to set the commit range: {1}
{0:?} is not a valid commit range. Did you provide the correct arguments?"
)]
SetCommitRangeError(String, #[source] git2::Error),
/// Error variant that represents other repository related errors.
#[cfg(feature = "repo")]
#[error("Git repository error: `{0}`")]
RepoError(String),
/// Error that may occur while parsing the config file.
#[error("Cannot parse config: `{0}`")]
ConfigError(#[from] config::ConfigError),
/// A possible error while initializing the logger.
#[error("Logger error: `{0}`")]
LoggerError(String),
/// When commit's not follow the conventional commit structure we throw this
/// error.
#[error("Cannot parse the commit: `{0}`")]
ParseError(#[from] git_conventional::Error),
/// Error that may occur while grouping commits.
#[error("Grouping error: `{0}`")]
GroupError(String),
/// Error that may occur while generating changelog.
#[error("Changelog error: `{0}`")]
ChangelogError(String),
/// Error that may occur while parsing the template.
#[error("Template parse error:\n{0}")]
TemplateParseError(String),
/// Error that may occur while rendering the template.
#[error("Template render error:\n{0}")]
TemplateRenderError(String),
/// Error that may occur while rendering the template.
#[error("Template render error:\n{0}\n{1}")]
TemplateRenderDetailedError(String, String),
/// Error that may occur during more general template operations.
#[error("Template error: `{0}`")]
TemplateError(#[from] tera::Error),
/// Error that may occur while parsing the command line arguments.
#[error("Argument error: `{0}`")]
ArgumentError(String),
/// Error that may occur while extracting the embedded content.
#[error("Embedded error: `{0}`")]
EmbeddedError(String),
/// Errors that may occur when deserializing types from TOML format.
#[error("Cannot parse TOML: `{0}`")]
DeserializeError(#[from] toml::de::Error),
/// Errors that may occur while de/serializing JSON format.
#[error("Cannot de/serialize JSON: `{0}`")]
JsonError(#[from] serde_json::Error),
/// Errors that may occur during parsing or compiling a regular expression.
#[error("Cannot parse/compile regex: `{0}`")]
RegexError(#[from] regex::Error),
/// Error that may occur due to system time related anomalies.
#[error("System time error: `{0}`")]
SystemTimeError(#[from] std::time::SystemTimeError),
/// Error that may occur while parsing integers.
#[error("Failed to parse integer: `{0}`")]
IntParseError(#[from] std::num::TryFromIntError),
/// Error that may occur while processing parsers that define field and
/// value matchers.
#[error("Field error: `{0}`")]
FieldError(String),
/// Error that may occur while parsing a `SemVer` version or version
/// requirement.
#[error("Semver error: `{0}`")]
SemverError(#[from] semver::Error),
/// The errors that may occur when processing a HTTP request.
#[error("HTTP client error: `{0}`")]
#[cfg(feature = "remote")]
HttpClientError(#[from] reqwest::Error),
/// The errors that may occur while constructing the HTTP client with
/// middleware.
#[error("HTTP client with middleware error: `{0}`")]
#[cfg(feature = "remote")]
HttpClientMiddlewareError(#[from] reqwest_middleware::Error),
/// A possible error when converting a `HeaderValue` from a string or byte
/// slice.
#[error("HTTP header error: `{0}`")]
#[cfg(feature = "remote")]
HttpHeaderError(#[from] reqwest::header::InvalidHeaderValue),
/// Error that may occur during handling pages.
#[error("Pagination error: `{0}`")]
PaginationError(String),
/// The errors that may occur while parsing URLs.
#[error("URL parse error: `{0}`")]
UrlParseError(#[from] url::ParseError),
/// Error that may occur when a remote is not set.
#[error("Repository remote is not set.")]
RemoteNotSetError,
/// Error that may occur while handling location of directories.
#[error("Directory error: `{0}`")]
DirsError(String),
/// Error that may occur while constructing patterns.
#[error("Pattern error: `{0}`")]
PatternError(#[from] glob::PatternError),
/// Error that may occur when unconventional commits are found.
/// See `require_conventional` option for more information.
#[error(
"Requiring all commits be conventional but found {0} unconventional \
commits."
)]
UnconventionalCommitsError(i32),
)]
SetCommitRangeError(String, #[source] git2::Error),
/// Error variant that represents other repository related errors.
#[cfg(feature = "repo")]
#[error("Git repository error: `{0}`")]
RepoError(String),
/// Error that may occur while parsing the config file.
#[error("Cannot parse config: `{0}`")]
ConfigError(#[from] config::ConfigError),
/// A possible error while initializing the logger.
#[error("Logger error: `{0}`")]
LoggerError(String),
/// When commit's not follow the conventional commit structure we throw this
/// error.
#[error("Cannot parse the commit: `{0}`")]
ParseError(#[from] git_conventional::Error),
/// Error that may occur while grouping commits.
#[error("Grouping error: `{0}`")]
GroupError(String),
/// Error that may occur while generating changelog.
#[error("Changelog error: `{0}`")]
ChangelogError(String),
/// Error that may occur while parsing the template.
#[error("Template parse error:\n{0}")]
TemplateParseError(String),
/// Error that may occur while rendering the template.
#[error("Template render error:\n{0}")]
TemplateRenderError(String),
/// Error that may occur while rendering the template.
#[error("Template render error:\n{0}\n{1}")]
TemplateRenderDetailedError(String, String),
/// Error that may occur during more general template operations.
#[error("Template error: `{0}`")]
TemplateError(#[from] tera::Error),
/// Error that may occur while parsing the command line arguments.
#[error("Argument error: `{0}`")]
ArgumentError(String),
/// Error that may occur while extracting the embedded content.
#[error("Embedded error: `{0}`")]
EmbeddedError(String),
/// Errors that may occur when deserializing types from TOML format.
#[error("Cannot parse TOML: `{0}`")]
DeserializeError(#[from] toml::de::Error),
/// Errors that may occur while de/serializing JSON format.
#[error("Cannot de/serialize JSON: `{0}`")]
JsonError(#[from] serde_json::Error),
/// Errors that may occur during parsing or compiling a regular expression.
#[error("Cannot parse/compile regex: `{0}`")]
RegexError(#[from] regex::Error),
/// Error that may occur due to system time related anomalies.
#[error("System time error: `{0}`")]
SystemTimeError(#[from] std::time::SystemTimeError),
/// Error that may occur while parsing integers.
#[error("Failed to parse integer: `{0}`")]
IntParseError(#[from] std::num::TryFromIntError),
/// Error that may occur while processing parsers that define field and
/// value matchers.
#[error("Field error: `{0}`")]
FieldError(String),
/// Error that may occur while parsing a `SemVer` version or version
/// requirement.
#[error("Semver error: `{0}`")]
SemverError(#[from] semver::Error),
/// The errors that may occur when processing a HTTP request.
#[error("HTTP client error: `{0}`")]
#[cfg(feature = "remote")]
HttpClientError(#[from] reqwest::Error),
/// The errors that may occur while constructing the HTTP client with
/// middleware.
#[error("HTTP client with middleware error: `{0}`")]
#[cfg(feature = "remote")]
HttpClientMiddlewareError(#[from] reqwest_middleware::Error),
/// A possible error when converting a `HeaderValue` from a string or byte
/// slice.
#[error("HTTP header error: `{0}`")]
#[cfg(feature = "remote")]
HttpHeaderError(#[from] reqwest::header::InvalidHeaderValue),
/// Error that may occur during handling pages.
#[error("Pagination error: `{0}`")]
PaginationError(String),
/// The errors that may occur while parsing URLs.
#[error("URL parse error: `{0}`")]
UrlParseError(#[from] url::ParseError),
/// Error that may occur when a remote is not set.
#[error("Repository remote is not set.")]
RemoteNotSetError,
/// Error that may occur while handling location of directories.
#[error("Directory error: `{0}`")]
DirsError(String),
/// Error that may occur while constructing patterns.
#[error("Pattern error: `{0}`")]
PatternError(#[from] glob::PatternError),
/// Error that may occur when unconventional commits are found.
/// See `require_conventional` option for more information.
#[error("Requiring all commits be conventional but found {0} unconventional commits.")]
UnconventionalCommitsError(i32),
}
/// Result type of the core library.
@@ -125,24 +122,24 @@ pub type Result<T> = core::result::Result<T, Error>;
#[cfg(test)]
mod test {
use git_conventional::{Commit, ErrorKind};
use git_conventional::{Commit, ErrorKind};
use super::*;
fn mock_function() -> super::Result<Commit<'static>> {
Ok(Commit::parse("test")?)
}
use super::*;
fn mock_function() -> super::Result<Commit<'static>> {
Ok(Commit::parse("test")?)
}
#[test]
fn throw_parse_error() {
let actual_error = mock_function().expect_err("expected error");
let expected_error_kind = ErrorKind::MissingType;
match actual_error {
Error::ParseError(e) => {
assert_eq!(expected_error_kind, e.kind());
}
_ => {
unreachable!()
}
}
}
#[test]
fn throw_parse_error() {
let actual_error = mock_function().expect_err("expected error");
let expected_error_kind = ErrorKind::MissingType;
match actual_error {
Error::ParseError(e) => {
assert_eq!(expected_error_kind, e.kind());
}
_ => {
unreachable!()
}
}
}
}
+2 -2
View File
@@ -8,8 +8,8 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![warn(missing_docs, clippy::unwrap_used)]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/orhun/git-cliff/main/website/static/img/git-cliff.png",
html_favicon_url = "https://raw.githubusercontent.com/orhun/git-cliff/main/website/static/favicon/favicon.ico"
html_logo_url = "https://raw.githubusercontent.com/orhun/git-cliff/main/website/static/img/git-cliff.png",
html_favicon_url = "https://raw.githubusercontent.com/orhun/git-cliff/main/website/static/favicon/favicon.ico"
)]
/// Changelog generator.
+1684 -1741
View File
File diff suppressed because it is too large Load Diff
+142 -163
View File
@@ -12,8 +12,7 @@ pub const START_FETCHING_MSG: &str = "Retrieving data from Bitbucket...";
pub const FINISHED_FETCHING_MSG: &str = "Done fetching Bitbucket data.";
/// Template variables related to this remote.
pub(crate) const TEMPLATE_VARIABLES: &[&str] =
&["bitbucket", "commit.bitbucket", "commit.remote"];
pub(crate) const TEMPLATE_VARIABLES: &[&str] = &["bitbucket", "commit.bitbucket", "commit.remote"];
/// Maximum number of entries to fetch for bitbucket pull requests.
pub(crate) const BITBUCKET_MAX_PAGE_PRS: usize = 50;
@@ -21,57 +20,51 @@ pub(crate) const BITBUCKET_MAX_PAGE_PRS: usize = 50;
/// Representation of a single commit.
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BitbucketCommit {
/// SHA.
pub hash: String,
/// Date of the commit
pub date: String,
/// Author of the commit.
pub author: Option<BitbucketCommitAuthor>,
/// SHA.
pub hash: String,
/// Date of the commit
pub date: String,
/// Author of the commit.
pub author: Option<BitbucketCommitAuthor>,
}
impl RemoteCommit for BitbucketCommit {
fn id(&self) -> String {
self.hash.clone()
}
fn id(&self) -> String {
self.hash.clone()
}
fn username(&self) -> Option<String> {
self.author.clone().and_then(|v| v.login)
}
fn username(&self) -> Option<String> {
self.author.clone().and_then(|v| v.login)
}
fn timestamp(&self) -> Option<i64> {
Some(self.convert_to_unix_timestamp(self.date.clone().as_str()))
}
fn timestamp(&self) -> Option<i64> {
Some(self.convert_to_unix_timestamp(self.date.clone().as_str()))
}
}
/// <https://developer.atlassian.com/cloud/bitbucket/rest/api-group-commits/#api-repositories-workspace-repo-slug-commits-get>
impl RemoteEntry for BitbucketPagination<BitbucketCommit> {
fn url(
_id: i64,
api_url: &str,
remote: &Remote,
ref_name: Option<&str>,
page: i32,
) -> String {
let commit_page = page + 1;
let mut url = format!(
"{}/{}/{}/commits?pagelen={MAX_PAGE_SIZE}&page={commit_page}",
api_url, remote.owner, remote.repo
);
fn url(_id: i64, api_url: &str, remote: &Remote, ref_name: Option<&str>, page: i32) -> String {
let commit_page = page + 1;
let mut url = format!(
"{}/{}/{}/commits?pagelen={MAX_PAGE_SIZE}&page={commit_page}",
api_url, remote.owner, remote.repo
);
if let Some(ref_name) = ref_name {
url.push_str(&format!("&include={}", ref_name));
}
if let Some(ref_name) = ref_name {
url.push_str(&format!("&include={}", ref_name));
}
url
}
url
}
fn buffer_size() -> usize {
10
}
fn buffer_size() -> usize {
10
}
fn early_exit(&self) -> bool {
self.values.is_empty()
}
fn early_exit(&self) -> bool {
self.values.is_empty()
}
}
/// Bitbucket Pagination Header
@@ -79,185 +72,171 @@ impl RemoteEntry for BitbucketPagination<BitbucketCommit> {
/// <https://developer.atlassian.com/cloud/bitbucket/rest/intro/#pagination>
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BitbucketPagination<T> {
/// Total number of objects in the response.
pub size: Option<i64>,
/// Page number of the current results.
pub page: Option<i64>,
/// Current number of objects on the existing page. Globally, the minimum
/// length is 10 and the maximum is 100.
pub pagelen: Option<i64>,
/// Link to the next page if it exists.
pub next: Option<String>,
/// Link to the previous page if it exists.
pub previous: Option<String>,
/// List of Objects.
pub values: Vec<T>,
/// Total number of objects in the response.
pub size: Option<i64>,
/// Page number of the current results.
pub page: Option<i64>,
/// Current number of objects on the existing page. Globally, the minimum
/// length is 10 and the maximum is 100.
pub pagelen: Option<i64>,
/// Link to the next page if it exists.
pub next: Option<String>,
/// Link to the previous page if it exists.
pub previous: Option<String>,
/// List of Objects.
pub values: Vec<T>,
}
/// Author of the commit.
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BitbucketCommitAuthor {
/// Username.
#[serde(rename = "raw")]
pub login: Option<String>,
/// Username.
#[serde(rename = "raw")]
pub login: Option<String>,
}
/// Label of the pull request.
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PullRequestLabel {
/// Name of the label.
pub name: String,
/// Name of the label.
pub name: String,
}
/// Representation of a single pull request's merge commit
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BitbucketPullRequestMergeCommit {
/// SHA of the merge commit.
pub hash: String,
/// SHA of the merge commit.
pub hash: String,
}
/// Representation of a single pull request.
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BitbucketPullRequest {
/// Pull request number.
pub id: i64,
/// Pull request title.
pub title: Option<String>,
/// Bitbucket Pull Request Merge Commit
pub merge_commit: BitbucketPullRequestMergeCommit,
/// Author of Pull Request
pub author: BitbucketCommitAuthor,
/// Pull request number.
pub id: i64,
/// Pull request title.
pub title: Option<String>,
/// Bitbucket Pull Request Merge Commit
pub merge_commit: BitbucketPullRequestMergeCommit,
/// Author of Pull Request
pub author: BitbucketCommitAuthor,
}
impl RemotePullRequest for BitbucketPullRequest {
fn number(&self) -> i64 {
self.id
}
fn number(&self) -> i64 {
self.id
}
fn title(&self) -> Option<String> {
self.title.clone()
}
fn title(&self) -> Option<String> {
self.title.clone()
}
fn labels(&self) -> Vec<String> {
vec![]
}
fn labels(&self) -> Vec<String> {
vec![]
}
fn merge_commit(&self) -> Option<String> {
Some(self.merge_commit.hash.clone())
}
fn merge_commit(&self) -> Option<String> {
Some(self.merge_commit.hash.clone())
}
}
/// <https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pullrequests/#api-repositories-workspace-repo-slug-pullrequests-get>
impl RemoteEntry for BitbucketPagination<BitbucketPullRequest> {
fn url(
_id: i64,
api_url: &str,
remote: &Remote,
_ref_name: Option<&str>,
page: i32,
) -> String {
let pr_page = page + 1;
format!(
"{}/{}/{}/pullrequests?&pagelen={BITBUCKET_MAX_PAGE_PRS}&\
page={pr_page}&state=MERGED",
api_url, remote.owner, remote.repo
)
}
fn url(_id: i64, api_url: &str, remote: &Remote, _ref_name: Option<&str>, page: i32) -> String {
let pr_page = page + 1;
format!(
"{}/{}/{}/pullrequests?&pagelen={BITBUCKET_MAX_PAGE_PRS}&page={pr_page}&state=MERGED",
api_url, remote.owner, remote.repo
)
}
fn buffer_size() -> usize {
5
}
fn buffer_size() -> usize {
5
}
fn early_exit(&self) -> bool {
self.values.is_empty()
}
fn early_exit(&self) -> bool {
self.values.is_empty()
}
}
/// HTTP client for handling Bitbucket REST API requests.
#[derive(Debug, Clone)]
pub struct BitbucketClient {
/// Remote.
remote: Remote,
/// HTTP client.
client: ClientWithMiddleware,
/// Remote.
remote: Remote,
/// HTTP client.
client: ClientWithMiddleware,
}
/// Constructs a Bitbucket client from the remote configuration.
impl TryFrom<Remote> for BitbucketClient {
type Error = Error;
fn try_from(remote: Remote) -> Result<Self> {
Ok(Self {
client: remote.create_client("application/json")?,
remote,
})
}
type Error = Error;
fn try_from(remote: Remote) -> Result<Self> {
Ok(Self {
client: remote.create_client("application/json")?,
remote,
})
}
}
impl RemoteClient for BitbucketClient {
const API_URL: &'static str = "https://api.bitbucket.org/2.0/repositories";
const API_URL_ENV: &'static str = "BITBUCKET_API_URL";
const API_URL: &'static str = "https://api.bitbucket.org/2.0/repositories";
const API_URL_ENV: &'static str = "BITBUCKET_API_URL";
fn remote(&self) -> Remote {
self.remote.clone()
}
fn remote(&self) -> Remote {
self.remote.clone()
}
fn client(&self) -> ClientWithMiddleware {
self.client.clone()
}
fn client(&self) -> ClientWithMiddleware {
self.client.clone()
}
}
impl BitbucketClient {
/// Fetches the Bitbucket API and returns the commits.
pub async fn get_commits(
&self,
ref_name: Option<&str>,
) -> Result<Vec<Box<dyn RemoteCommit>>> {
Ok(self
.fetch_with_early_exit::<BitbucketPagination<BitbucketCommit>>(
0, ref_name,
)
.await?
.into_iter()
.flat_map(|v| v.values)
.map(|v| Box::new(v) as Box<dyn RemoteCommit>)
.collect())
}
/// Fetches the Bitbucket API and returns the commits.
pub async fn get_commits(&self, ref_name: Option<&str>) -> Result<Vec<Box<dyn RemoteCommit>>> {
Ok(self
.fetch_with_early_exit::<BitbucketPagination<BitbucketCommit>>(0, ref_name)
.await?
.into_iter()
.flat_map(|v| v.values)
.map(|v| Box::new(v) as Box<dyn RemoteCommit>)
.collect())
}
/// Fetches the Bitbucket API and returns the pull requests.
pub async fn get_pull_requests(
&self,
ref_name: Option<&str>,
) -> Result<Vec<Box<dyn RemotePullRequest>>> {
Ok(self
.fetch_with_early_exit::<BitbucketPagination<BitbucketPullRequest>>(
0, ref_name,
)
.await?
.into_iter()
.flat_map(|v| v.values)
.map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
.collect())
}
/// Fetches the Bitbucket API and returns the pull requests.
pub async fn get_pull_requests(
&self,
ref_name: Option<&str>,
) -> Result<Vec<Box<dyn RemotePullRequest>>> {
Ok(self
.fetch_with_early_exit::<BitbucketPagination<BitbucketPullRequest>>(0, ref_name)
.await?
.into_iter()
.flat_map(|v| v.values)
.map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
.collect())
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use pretty_assertions::assert_eq;
use super::*;
use crate::remote::RemoteCommit;
use super::*;
use crate::remote::RemoteCommit;
#[test]
fn timestamp() {
let remote_commit = BitbucketCommit {
hash: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
author: Some(BitbucketCommitAuthor {
login: Some(String::from("orhun")),
}),
date: String::from("2021-07-18T15:14:39+03:00"),
};
#[test]
fn timestamp() {
let remote_commit = BitbucketCommit {
hash: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
author: Some(BitbucketCommitAuthor {
login: Some(String::from("orhun")),
}),
date: String::from("2021-07-18T15:14:39+03:00"),
};
assert_eq!(Some(1626610479), remote_commit.timestamp());
}
assert_eq!(Some(1626610479), remote_commit.timestamp());
}
}
+122 -139
View File
@@ -12,211 +12,194 @@ pub const START_FETCHING_MSG: &str = "Retrieving data from Gitea...";
pub const FINISHED_FETCHING_MSG: &str = "Done fetching Gitea data.";
/// Template variables related to this remote.
pub(crate) const TEMPLATE_VARIABLES: &[&str] =
&["gitea", "commit.gitea", "commit.remote"];
pub(crate) const TEMPLATE_VARIABLES: &[&str] = &["gitea", "commit.gitea", "commit.remote"];
/// Representation of a single commit.
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GiteaCommit {
/// SHA.
pub sha: String,
/// Author of the commit.
pub author: Option<GiteaCommitAuthor>,
/// Timestamp of the commit.
pub created: String,
/// SHA.
pub sha: String,
/// Author of the commit.
pub author: Option<GiteaCommitAuthor>,
/// Timestamp of the commit.
pub created: String,
}
impl RemoteCommit for GiteaCommit {
fn id(&self) -> String {
self.sha.clone()
}
fn id(&self) -> String {
self.sha.clone()
}
fn username(&self) -> Option<String> {
self.author.clone().and_then(|v| v.login)
}
fn username(&self) -> Option<String> {
self.author.clone().and_then(|v| v.login)
}
fn timestamp(&self) -> Option<i64> {
Some(self.convert_to_unix_timestamp(self.created.clone().as_str()))
}
fn timestamp(&self) -> Option<i64> {
Some(self.convert_to_unix_timestamp(self.created.clone().as_str()))
}
}
impl RemoteEntry for GiteaCommit {
fn url(
_id: i64,
api_url: &str,
remote: &Remote,
ref_name: Option<&str>,
page: i32,
) -> String {
let mut url = format!(
"{}/api/v1/repos/{}/{}/commits?limit={MAX_PAGE_SIZE}&page={page}",
api_url, remote.owner, remote.repo
);
fn url(_id: i64, api_url: &str, remote: &Remote, ref_name: Option<&str>, page: i32) -> String {
let mut url = format!(
"{}/api/v1/repos/{}/{}/commits?limit={MAX_PAGE_SIZE}&page={page}",
api_url, remote.owner, remote.repo
);
if let Some(ref_name) = ref_name {
url.push_str(&format!("&sha={}", ref_name));
}
if let Some(ref_name) = ref_name {
url.push_str(&format!("&sha={}", ref_name));
}
url
}
url
}
fn buffer_size() -> usize {
10
}
fn buffer_size() -> usize {
10
}
fn early_exit(&self) -> bool {
false
}
fn early_exit(&self) -> bool {
false
}
}
/// Author of the commit.
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GiteaCommitAuthor {
/// Username.
pub login: Option<String>,
/// Username.
pub login: Option<String>,
}
/// Label of the pull request.
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PullRequestLabel {
/// Name of the label.
pub name: String,
/// Name of the label.
pub name: String,
}
/// Representation of a single pull request.
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GiteaPullRequest {
/// Pull request number.
pub number: i64,
/// Pull request title.
pub title: Option<String>,
/// SHA of the merge commit.
pub merge_commit_sha: Option<String>,
/// Labels of the pull request.
pub labels: Vec<PullRequestLabel>,
/// Pull request number.
pub number: i64,
/// Pull request title.
pub title: Option<String>,
/// SHA of the merge commit.
pub merge_commit_sha: Option<String>,
/// Labels of the pull request.
pub labels: Vec<PullRequestLabel>,
}
impl RemotePullRequest for GiteaPullRequest {
fn number(&self) -> i64 {
self.number
}
fn number(&self) -> i64 {
self.number
}
fn title(&self) -> Option<String> {
self.title.clone()
}
fn title(&self) -> Option<String> {
self.title.clone()
}
fn labels(&self) -> Vec<String> {
self.labels.iter().map(|v| v.name.clone()).collect()
}
fn labels(&self) -> Vec<String> {
self.labels.iter().map(|v| v.name.clone()).collect()
}
fn merge_commit(&self) -> Option<String> {
self.merge_commit_sha.clone()
}
fn merge_commit(&self) -> Option<String> {
self.merge_commit_sha.clone()
}
}
impl RemoteEntry for GiteaPullRequest {
fn url(
_id: i64,
api_url: &str,
remote: &Remote,
_ref_name: Option<&str>,
page: i32,
) -> String {
format!(
"{}/api/v1/repos/{}/{}/pulls?limit={MAX_PAGE_SIZE}&page={page}&\
state=closed",
api_url, remote.owner, remote.repo
)
}
fn url(_id: i64, api_url: &str, remote: &Remote, _ref_name: Option<&str>, page: i32) -> String {
format!(
"{}/api/v1/repos/{}/{}/pulls?limit={MAX_PAGE_SIZE}&page={page}&state=closed",
api_url, remote.owner, remote.repo
)
}
fn buffer_size() -> usize {
5
}
fn buffer_size() -> usize {
5
}
fn early_exit(&self) -> bool {
false
}
fn early_exit(&self) -> bool {
false
}
}
/// HTTP client for handling Gitea REST API requests.
#[derive(Debug, Clone)]
pub struct GiteaClient {
/// Remote.
remote: Remote,
/// HTTP client.
client: ClientWithMiddleware,
/// Remote.
remote: Remote,
/// HTTP client.
client: ClientWithMiddleware,
}
/// Constructs a Gitea client from the remote configuration.
impl TryFrom<Remote> for GiteaClient {
type Error = Error;
fn try_from(remote: Remote) -> Result<Self> {
Ok(Self {
client: remote.create_client("application/json")?,
remote,
})
}
type Error = Error;
fn try_from(remote: Remote) -> Result<Self> {
Ok(Self {
client: remote.create_client("application/json")?,
remote,
})
}
}
impl RemoteClient for GiteaClient {
const API_URL: &'static str = "https://codeberg.org";
const API_URL_ENV: &'static str = "GITEA_API_URL";
const API_URL: &'static str = "https://codeberg.org";
const API_URL_ENV: &'static str = "GITEA_API_URL";
fn remote(&self) -> Remote {
self.remote.clone()
}
fn remote(&self) -> Remote {
self.remote.clone()
}
fn client(&self) -> ClientWithMiddleware {
self.client.clone()
}
fn client(&self) -> ClientWithMiddleware {
self.client.clone()
}
}
impl GiteaClient {
/// Fetches the Gitea API and returns the commits.
pub async fn get_commits(
&self,
ref_name: Option<&str>,
) -> Result<Vec<Box<dyn RemoteCommit>>> {
Ok(self
.fetch::<GiteaCommit>(0, ref_name)
.await?
.into_iter()
.map(|v| Box::new(v) as Box<dyn RemoteCommit>)
.collect())
}
/// Fetches the Gitea API and returns the commits.
pub async fn get_commits(&self, ref_name: Option<&str>) -> Result<Vec<Box<dyn RemoteCommit>>> {
Ok(self
.fetch::<GiteaCommit>(0, ref_name)
.await?
.into_iter()
.map(|v| Box::new(v) as Box<dyn RemoteCommit>)
.collect())
}
/// Fetches the Gitea API and returns the pull requests.
pub async fn get_pull_requests(
&self,
ref_name: Option<&str>,
) -> Result<Vec<Box<dyn RemotePullRequest>>> {
Ok(self
.fetch::<GiteaPullRequest>(0, ref_name)
.await?
.into_iter()
.map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
.collect())
}
/// Fetches the Gitea API and returns the pull requests.
pub async fn get_pull_requests(
&self,
ref_name: Option<&str>,
) -> Result<Vec<Box<dyn RemotePullRequest>>> {
Ok(self
.fetch::<GiteaPullRequest>(0, ref_name)
.await?
.into_iter()
.map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
.collect())
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use pretty_assertions::assert_eq;
use super::*;
use crate::remote::RemoteCommit;
use super::*;
use crate::remote::RemoteCommit;
#[test]
fn timestamp() {
let remote_commit = GiteaCommit {
sha: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
author: Some(GiteaCommitAuthor {
login: Some(String::from("orhun")),
}),
created: String::from("2021-07-18T15:14:39+03:00"),
};
#[test]
fn timestamp() {
let remote_commit = GiteaCommit {
sha: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
author: Some(GiteaCommitAuthor {
login: Some(String::from("orhun")),
}),
created: String::from("2021-07-18T15:14:39+03:00"),
};
assert_eq!(Some(1626610479), remote_commit.timestamp());
}
assert_eq!(Some(1626610479), remote_commit.timestamp());
}
}
+132 -148
View File
@@ -12,230 +12,214 @@ pub const START_FETCHING_MSG: &str = "Retrieving data from GitHub...";
pub const FINISHED_FETCHING_MSG: &str = "Done fetching GitHub data.";
/// Template variables related to this remote.
pub(crate) const TEMPLATE_VARIABLES: &[&str] =
&["github", "commit.github", "commit.remote"];
pub(crate) const TEMPLATE_VARIABLES: &[&str] = &["github", "commit.github", "commit.remote"];
/// Representation of a single commit.
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GitHubCommit {
/// SHA.
pub sha: String,
/// Author of the commit.
pub author: Option<GitHubCommitAuthor>,
/// Details of the commit
pub commit: Option<GitHubCommitDetails>,
/// SHA.
pub sha: String,
/// Author of the commit.
pub author: Option<GitHubCommitAuthor>,
/// Details of the commit
pub commit: Option<GitHubCommitDetails>,
}
/// Representation of subset of commit details
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GitHubCommitDetails {
/// Author of the commit
pub author: GitHubCommitDetailsAuthor,
/// Author of the commit
pub author: GitHubCommitDetailsAuthor,
}
/// Representation of subset of commit author details
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GitHubCommitDetailsAuthor {
/// Date of the commit
pub date: String,
/// Date of the commit
pub date: String,
}
impl RemoteCommit for GitHubCommit {
fn id(&self) -> String {
self.sha.clone()
}
fn id(&self) -> String {
self.sha.clone()
}
fn username(&self) -> Option<String> {
self.author.clone().and_then(|v| v.login)
}
fn username(&self) -> Option<String> {
self.author.clone().and_then(|v| v.login)
}
fn timestamp(&self) -> Option<i64> {
self.commit
.clone()
.map(|f| self.convert_to_unix_timestamp(f.author.date.clone().as_str()))
}
fn timestamp(&self) -> Option<i64> {
self.commit
.clone()
.map(|f| self.convert_to_unix_timestamp(f.author.date.clone().as_str()))
}
}
impl RemoteEntry for GitHubCommit {
fn url(
_id: i64,
api_url: &str,
remote: &Remote,
ref_name: Option<&str>,
page: i32,
) -> String {
let mut url = format!(
"{}/repos/{}/{}/commits?per_page={MAX_PAGE_SIZE}&page={page}",
api_url, remote.owner, remote.repo
);
fn url(_id: i64, api_url: &str, remote: &Remote, ref_name: Option<&str>, page: i32) -> String {
let mut url = format!(
"{}/repos/{}/{}/commits?per_page={MAX_PAGE_SIZE}&page={page}",
api_url, remote.owner, remote.repo
);
if let Some(ref_name) = ref_name {
url.push_str(&format!("&sha={}", ref_name));
}
if let Some(ref_name) = ref_name {
url.push_str(&format!("&sha={}", ref_name));
}
url
}
url
}
fn buffer_size() -> usize {
10
}
fn buffer_size() -> usize {
10
}
fn early_exit(&self) -> bool {
false
}
fn early_exit(&self) -> bool {
false
}
}
/// Author of the commit.
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GitHubCommitAuthor {
/// Username.
pub login: Option<String>,
/// Username.
pub login: Option<String>,
}
/// Label of the pull request.
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PullRequestLabel {
/// Name of the label.
pub name: String,
/// Name of the label.
pub name: String,
}
/// Representation of a single pull request.
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GitHubPullRequest {
/// Pull request number.
pub number: i64,
/// Pull request title.
pub title: Option<String>,
/// SHA of the merge commit.
pub merge_commit_sha: Option<String>,
/// Labels of the pull request.
pub labels: Vec<PullRequestLabel>,
/// Pull request number.
pub number: i64,
/// Pull request title.
pub title: Option<String>,
/// SHA of the merge commit.
pub merge_commit_sha: Option<String>,
/// Labels of the pull request.
pub labels: Vec<PullRequestLabel>,
}
impl RemotePullRequest for GitHubPullRequest {
fn number(&self) -> i64 {
self.number
}
fn number(&self) -> i64 {
self.number
}
fn title(&self) -> Option<String> {
self.title.clone()
}
fn title(&self) -> Option<String> {
self.title.clone()
}
fn labels(&self) -> Vec<String> {
self.labels.iter().map(|v| v.name.clone()).collect()
}
fn labels(&self) -> Vec<String> {
self.labels.iter().map(|v| v.name.clone()).collect()
}
fn merge_commit(&self) -> Option<String> {
self.merge_commit_sha.clone()
}
fn merge_commit(&self) -> Option<String> {
self.merge_commit_sha.clone()
}
}
impl RemoteEntry for GitHubPullRequest {
fn url(
_id: i64,
api_url: &str,
remote: &Remote,
_ref_name: Option<&str>,
page: i32,
) -> String {
format!(
"{}/repos/{}/{}/pulls?per_page={MAX_PAGE_SIZE}&page={page}&state=closed",
api_url, remote.owner, remote.repo
)
}
fn url(_id: i64, api_url: &str, remote: &Remote, _ref_name: Option<&str>, page: i32) -> String {
format!(
"{}/repos/{}/{}/pulls?per_page={MAX_PAGE_SIZE}&page={page}&state=closed",
api_url, remote.owner, remote.repo
)
}
fn buffer_size() -> usize {
5
}
fn buffer_size() -> usize {
5
}
fn early_exit(&self) -> bool {
false
}
fn early_exit(&self) -> bool {
false
}
}
/// HTTP client for handling GitHub REST API requests.
#[derive(Debug, Clone)]
pub struct GitHubClient {
/// Remote.
remote: Remote,
/// HTTP client.
client: ClientWithMiddleware,
/// Remote.
remote: Remote,
/// HTTP client.
client: ClientWithMiddleware,
}
/// Constructs a GitHub client from the remote configuration.
impl TryFrom<Remote> for GitHubClient {
type Error = Error;
fn try_from(remote: Remote) -> Result<Self> {
Ok(Self {
client: remote.create_client("application/vnd.github+json")?,
remote,
})
}
type Error = Error;
fn try_from(remote: Remote) -> Result<Self> {
Ok(Self {
client: remote.create_client("application/vnd.github+json")?,
remote,
})
}
}
impl RemoteClient for GitHubClient {
const API_URL: &'static str = "https://api.github.com";
const API_URL_ENV: &'static str = "GITHUB_API_URL";
const API_URL: &'static str = "https://api.github.com";
const API_URL_ENV: &'static str = "GITHUB_API_URL";
fn remote(&self) -> Remote {
self.remote.clone()
}
fn remote(&self) -> Remote {
self.remote.clone()
}
fn client(&self) -> ClientWithMiddleware {
self.client.clone()
}
fn client(&self) -> ClientWithMiddleware {
self.client.clone()
}
}
impl GitHubClient {
/// Fetches the GitHub API and returns the commits.
pub async fn get_commits(
&self,
ref_name: Option<&str>,
) -> Result<Vec<Box<dyn RemoteCommit>>> {
Ok(self
.fetch::<GitHubCommit>(0, ref_name)
.await?
.into_iter()
.map(|v| Box::new(v) as Box<dyn RemoteCommit>)
.collect())
}
/// Fetches the GitHub API and returns the commits.
pub async fn get_commits(&self, ref_name: Option<&str>) -> Result<Vec<Box<dyn RemoteCommit>>> {
Ok(self
.fetch::<GitHubCommit>(0, ref_name)
.await?
.into_iter()
.map(|v| Box::new(v) as Box<dyn RemoteCommit>)
.collect())
}
/// Fetches the GitHub API and returns the pull requests.
pub async fn get_pull_requests(
&self,
ref_name: Option<&str>,
) -> Result<Vec<Box<dyn RemotePullRequest>>> {
Ok(self
.fetch::<GitHubPullRequest>(0, ref_name)
.await?
.into_iter()
.map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
.collect())
}
/// Fetches the GitHub API and returns the pull requests.
pub async fn get_pull_requests(
&self,
ref_name: Option<&str>,
) -> Result<Vec<Box<dyn RemotePullRequest>>> {
Ok(self
.fetch::<GitHubPullRequest>(0, ref_name)
.await?
.into_iter()
.map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
.collect())
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use pretty_assertions::assert_eq;
use super::*;
use crate::remote::RemoteCommit;
use super::*;
use crate::remote::RemoteCommit;
#[test]
fn timestamp() {
let remote_commit = GitHubCommit {
sha: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
author: Some(GitHubCommitAuthor {
login: Some(String::from("orhun")),
}),
commit: Some(GitHubCommitDetails {
author: GitHubCommitDetailsAuthor {
date: String::from("2021-07-18T15:14:39+03:00"),
},
}),
};
#[test]
fn timestamp() {
let remote_commit = GitHubCommit {
sha: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
author: Some(GitHubCommitAuthor {
login: Some(String::from("orhun")),
}),
commit: Some(GitHubCommitDetails {
author: GitHubCommitDetailsAuthor {
date: String::from("2021-07-18T15:14:39+03:00"),
},
}),
};
assert_eq!(Some(1626610479), remote_commit.timestamp());
}
assert_eq!(Some(1626610479), remote_commit.timestamp());
}
}
+243 -269
View File
@@ -12,53 +12,52 @@ pub const START_FETCHING_MSG: &str = "Retrieving data from GitLab...";
pub const FINISHED_FETCHING_MSG: &str = "Done fetching GitLab data.";
/// Template variables related to this remote.
pub(crate) const TEMPLATE_VARIABLES: &[&str] =
&["gitlab", "commit.gitlab", "commit.remote"];
pub(crate) const TEMPLATE_VARIABLES: &[&str] = &["gitlab", "commit.gitlab", "commit.remote"];
/// Representation of a single GitLab Project.
///
/// <https://docs.gitlab.com/ee/api/projects.html#get-single-project>
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GitLabProject {
/// GitLab id for project
pub id: i64,
/// Optional Description of project
pub description: Option<String>,
/// Name of project
pub name: String,
/// Name of project with namespace owner / repo
pub name_with_namespace: String,
/// Name of project with namespace owner/repo
pub path_with_namespace: String,
/// Project created at
pub created_at: String,
/// Default branch eg (main/master)
pub default_branch: String,
/// GitLab id for project
pub id: i64,
/// Optional Description of project
pub description: Option<String>,
/// Name of project
pub name: String,
/// Name of project with namespace owner / repo
pub name_with_namespace: String,
/// Name of project with namespace owner/repo
pub path_with_namespace: String,
/// Project created at
pub created_at: String,
/// Default branch eg (main/master)
pub default_branch: String,
}
impl RemoteEntry for GitLabProject {
fn url(
_id: i64,
api_url: &str,
remote: &Remote,
_ref_name: Option<&str>,
_page: i32,
) -> String {
format!(
"{}/projects/{}%2F{}",
api_url,
urlencoding::encode(remote.owner.as_str()),
remote.repo
)
}
fn url(
_id: i64,
api_url: &str,
remote: &Remote,
_ref_name: Option<&str>,
_page: i32,
) -> String {
format!(
"{}/projects/{}%2F{}",
api_url,
urlencoding::encode(remote.owner.as_str()),
remote.repo
)
}
fn buffer_size() -> usize {
1
}
fn buffer_size() -> usize {
1
}
fn early_exit(&self) -> bool {
false
}
fn early_exit(&self) -> bool {
false
}
}
/// Representation of a single commit.
@@ -66,77 +65,70 @@ impl RemoteEntry for GitLabProject {
/// <https://docs.gitlab.com/ee/api/commits.html>
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GitLabCommit {
/// Sha
pub id: String,
/// Short Sha
pub short_id: String,
/// Git message
pub title: String,
/// Author
pub author_name: String,
/// Author Email
pub author_email: String,
/// Authored Date
pub authored_date: String,
/// Committer Name
pub committer_name: String,
/// Committer Email
pub committer_email: String,
/// Committed Date
pub committed_date: String,
/// Created At
pub created_at: String,
/// Git Message
pub message: String,
/// Parent Ids
pub parent_ids: Vec<String>,
/// Web Url
pub web_url: String,
/// Sha
pub id: String,
/// Short Sha
pub short_id: String,
/// Git message
pub title: String,
/// Author
pub author_name: String,
/// Author Email
pub author_email: String,
/// Authored Date
pub authored_date: String,
/// Committer Name
pub committer_name: String,
/// Committer Email
pub committer_email: String,
/// Committed Date
pub committed_date: String,
/// Created At
pub created_at: String,
/// Git Message
pub message: String,
/// Parent Ids
pub parent_ids: Vec<String>,
/// Web Url
pub web_url: String,
}
impl RemoteCommit for GitLabCommit {
fn id(&self) -> String {
self.id.clone()
}
fn id(&self) -> String {
self.id.clone()
}
fn username(&self) -> Option<String> {
Some(self.author_name.clone())
}
fn username(&self) -> Option<String> {
Some(self.author_name.clone())
}
fn timestamp(&self) -> Option<i64> {
Some(self.convert_to_unix_timestamp(self.committed_date.clone().as_str()))
}
fn timestamp(&self) -> Option<i64> {
Some(self.convert_to_unix_timestamp(self.committed_date.clone().as_str()))
}
}
impl RemoteEntry for GitLabCommit {
fn url(
id: i64,
api_url: &str,
_remote: &Remote,
ref_name: Option<&str>,
page: i32,
) -> String {
let commit_page = page + 1;
let mut url = format!(
"{}/projects/{}/repository/commits?per_page={MAX_PAGE_SIZE}&\
page={commit_page}",
api_url, id
);
fn url(id: i64, api_url: &str, _remote: &Remote, ref_name: Option<&str>, page: i32) -> String {
let commit_page = page + 1;
let mut url = format!(
"{}/projects/{}/repository/commits?per_page={MAX_PAGE_SIZE}&page={commit_page}",
api_url, id
);
if let Some(ref_name) = ref_name {
url.push_str(&format!("&ref_name={}", ref_name));
}
if let Some(ref_name) = ref_name {
url.push_str(&format!("&ref_name={}", ref_name));
}
url
}
url
}
fn buffer_size() -> usize {
10
}
fn buffer_size() -> usize {
10
}
fn early_exit(&self) -> bool {
false
}
fn early_exit(&self) -> bool {
false
}
}
/// Representation of a single pull request.
@@ -144,227 +136,209 @@ impl RemoteEntry for GitLabCommit {
/// <https://docs.gitlab.com/ee/api/merge_requests.html>
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GitLabMergeRequest {
/// Id
pub id: i64,
/// Iid
pub iid: i64,
/// Project Id
pub project_id: i64,
/// Title
pub title: String,
/// Description
pub description: String,
/// State
pub state: String,
/// Created At
pub created_at: String,
/// Author
pub author: GitLabUser,
/// Commit Sha
pub sha: String,
/// Merge Commit Sha
pub merge_commit_sha: Option<String>,
/// Squash Commit Sha
pub squash_commit_sha: Option<String>,
/// Web Url
pub web_url: String,
/// Labels
pub labels: Vec<String>,
/// Id
pub id: i64,
/// Iid
pub iid: i64,
/// Project Id
pub project_id: i64,
/// Title
pub title: String,
/// Description
pub description: String,
/// State
pub state: String,
/// Created At
pub created_at: String,
/// Author
pub author: GitLabUser,
/// Commit Sha
pub sha: String,
/// Merge Commit Sha
pub merge_commit_sha: Option<String>,
/// Squash Commit Sha
pub squash_commit_sha: Option<String>,
/// Web Url
pub web_url: String,
/// Labels
pub labels: Vec<String>,
}
impl RemotePullRequest for GitLabMergeRequest {
fn number(&self) -> i64 {
self.iid
}
fn number(&self) -> i64 {
self.iid
}
fn title(&self) -> Option<String> {
Some(self.title.clone())
}
fn title(&self) -> Option<String> {
Some(self.title.clone())
}
fn labels(&self) -> Vec<String> {
self.labels.clone()
}
fn labels(&self) -> Vec<String> {
self.labels.clone()
}
fn merge_commit(&self) -> Option<String> {
self.merge_commit_sha
.clone()
.or(self.squash_commit_sha.clone())
.or(Some(self.sha.clone()))
}
fn merge_commit(&self) -> Option<String> {
self.merge_commit_sha
.clone()
.or(self.squash_commit_sha.clone())
.or(Some(self.sha.clone()))
}
}
impl RemoteEntry for GitLabMergeRequest {
fn url(
id: i64,
api_url: &str,
_remote: &Remote,
_ref_name: Option<&str>,
page: i32,
) -> String {
format!(
"{}/projects/{}/merge_requests?per_page={MAX_PAGE_SIZE}&page={page}&\
state=merged",
api_url, id
)
}
fn url(id: i64, api_url: &str, _remote: &Remote, _ref_name: Option<&str>, page: i32) -> String {
format!(
"{}/projects/{}/merge_requests?per_page={MAX_PAGE_SIZE}&page={page}&state=merged",
api_url, id
)
}
fn buffer_size() -> usize {
5
}
fn buffer_size() -> usize {
5
}
fn early_exit(&self) -> bool {
false
}
fn early_exit(&self) -> bool {
false
}
}
/// Representation of a GitLab User.
#[derive(Debug, Default, Clone, Hash, Eq, PartialEq, Deserialize, Serialize)]
pub struct GitLabUser {
/// Id
pub id: i64,
/// Name
pub name: String,
/// Username
pub username: String,
/// State of the User
pub state: String,
/// Url for avatar
pub avatar_url: Option<String>,
/// Web Url
pub web_url: String,
/// Id
pub id: i64,
/// Name
pub name: String,
/// Username
pub username: String,
/// State of the User
pub state: String,
/// Url for avatar
pub avatar_url: Option<String>,
/// Web Url
pub web_url: String,
}
/// Representation of a GitLab Reference.
#[derive(Debug, Default, Clone, Hash, Eq, PartialEq, Deserialize, Serialize)]
pub struct GitLabReference {
/// Short id
pub short: String,
/// Relative Link
pub relative: String,
/// Full Link
pub full: String,
/// Short id
pub short: String,
/// Relative Link
pub relative: String,
/// Full Link
pub full: String,
}
/// HTTP client for handling GitLab REST API requests.
#[derive(Debug, Clone)]
pub struct GitLabClient {
/// Remote.
remote: Remote,
/// HTTP client.
client: ClientWithMiddleware,
/// Remote.
remote: Remote,
/// HTTP client.
client: ClientWithMiddleware,
}
/// Constructs a GitLab client from the remote configuration.
impl TryFrom<Remote> for GitLabClient {
type Error = Error;
fn try_from(remote: Remote) -> Result<Self> {
Ok(Self {
client: remote.create_client("application/json")?,
remote,
})
}
type Error = Error;
fn try_from(remote: Remote) -> Result<Self> {
Ok(Self {
client: remote.create_client("application/json")?,
remote,
})
}
}
impl RemoteClient for GitLabClient {
const API_URL: &'static str = "https://gitlab.com/api/v4";
const API_URL_ENV: &'static str = "GITLAB_API_URL";
const API_URL: &'static str = "https://gitlab.com/api/v4";
const API_URL_ENV: &'static str = "GITLAB_API_URL";
fn remote(&self) -> Remote {
self.remote.clone()
}
fn remote(&self) -> Remote {
self.remote.clone()
}
fn client(&self) -> ClientWithMiddleware {
self.client.clone()
}
fn client(&self) -> ClientWithMiddleware {
self.client.clone()
}
}
impl GitLabClient {
/// Fetches the GitLab API and returns the pull requests.
pub async fn get_project(
&self,
ref_name: Option<&str>,
) -> Result<GitLabProject> {
self.get_entry::<GitLabProject>(0, ref_name, 1).await
}
/// Fetches the GitLab API and returns the pull requests.
pub async fn get_project(&self, ref_name: Option<&str>) -> Result<GitLabProject> {
self.get_entry::<GitLabProject>(0, ref_name, 1).await
}
/// Fetches the GitLab API and returns the commits.
pub async fn get_commits(
&self,
project_id: i64,
ref_name: Option<&str>,
) -> Result<Vec<Box<dyn RemoteCommit>>> {
Ok(self
.fetch::<GitLabCommit>(project_id, ref_name)
.await?
.into_iter()
.map(|v| Box::new(v) as Box<dyn RemoteCommit>)
.collect())
}
/// Fetches the GitLab API and returns the commits.
pub async fn get_commits(
&self,
project_id: i64,
ref_name: Option<&str>,
) -> Result<Vec<Box<dyn RemoteCommit>>> {
Ok(self
.fetch::<GitLabCommit>(project_id, ref_name)
.await?
.into_iter()
.map(|v| Box::new(v) as Box<dyn RemoteCommit>)
.collect())
}
/// Fetches the GitLab API and returns the pull requests.
pub async fn get_merge_requests(
&self,
project_id: i64,
ref_name: Option<&str>,
) -> Result<Vec<Box<dyn RemotePullRequest>>> {
Ok(self
.fetch::<GitLabMergeRequest>(project_id, ref_name)
.await?
.into_iter()
.map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
.collect())
}
/// Fetches the GitLab API and returns the pull requests.
pub async fn get_merge_requests(
&self,
project_id: i64,
ref_name: Option<&str>,
) -> Result<Vec<Box<dyn RemotePullRequest>>> {
Ok(self
.fetch::<GitLabMergeRequest>(project_id, ref_name)
.await?
.into_iter()
.map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
.collect())
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use pretty_assertions::assert_eq;
use super::*;
use super::*;
#[test]
fn gitlab_remote_encodes_owner() {
let remote = Remote::new("abc/def", "xyz1");
assert_eq!(
"https://gitlab.test.com/api/v4/projects/abc%2Fdef%2Fxyz1",
GitLabProject::url(
1,
"https://gitlab.test.com/api/v4",
&remote,
None,
0
)
);
}
#[test]
fn gitlab_remote_encodes_owner() {
let remote = Remote::new("abc/def", "xyz1");
assert_eq!(
"https://gitlab.test.com/api/v4/projects/abc%2Fdef%2Fxyz1",
GitLabProject::url(1, "https://gitlab.test.com/api/v4", &remote, None, 0)
);
}
#[test]
fn timestamp() {
let remote_commit = GitLabCommit {
id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
author_name: String::from("orhun"),
committed_date: String::from("2021-07-18T15:14:39+03:00"),
..Default::default()
};
#[test]
fn timestamp() {
let remote_commit = GitLabCommit {
id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
author_name: String::from("orhun"),
committed_date: String::from("2021-07-18T15:14:39+03:00"),
..Default::default()
};
assert_eq!(Some(1626610479), remote_commit.timestamp());
}
assert_eq!(Some(1626610479), remote_commit.timestamp());
}
#[test]
fn merge_request_no_merge_commit() {
let mr = GitLabMergeRequest {
sha: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
..Default::default()
};
assert!(mr.merge_commit().is_some());
}
#[test]
fn merge_request_no_merge_commit() {
let mr = GitLabMergeRequest {
sha: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
..Default::default()
};
assert!(mr.merge_commit().is_some());
}
#[test]
fn merge_request_squash_commit() {
let mr = GitLabMergeRequest {
squash_commit_sha: Some(String::from(
"1d244937ee6ceb8e0314a4a201ba93a7a61f2071",
)),
..Default::default()
};
assert!(mr.merge_commit().is_some());
}
#[test]
fn merge_request_squash_commit() {
let mr = GitLabMergeRequest {
squash_commit_sha: Some(String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071")),
..Default::default()
};
assert!(mr.merge_commit().is_some());
}
}
+285 -297
View File
@@ -20,9 +20,7 @@ use std::time::Duration;
use dyn_clone::DynClone;
use futures::{StreamExt, future, stream};
use http_cache_reqwest::{
CACacheManager, Cache, CacheMode, HttpCache, HttpCacheOptions,
};
use http_cache_reqwest::{CACacheManager, Cache, CacheMode, HttpCache, HttpCacheOptions};
use reqwest::Client;
use reqwest::header::{HeaderMap, HeaderValue};
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
@@ -39,8 +37,7 @@ use crate::error::{Error, Result};
/// User agent for interacting with the GitHub API.
///
/// This is needed since GitHub API does not accept empty user agent.
pub(crate) const USER_AGENT: &str =
concat!(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
pub(crate) const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
/// Request timeout value in seconds.
pub(crate) const REQUEST_TIMEOUT: u64 = 30;
@@ -53,337 +50,328 @@ pub(crate) const MAX_PAGE_SIZE: usize = 100;
/// Trait for handling the different entries returned from the remote.
pub trait RemoteEntry {
/// Returns the API URL for fetching the entries at the specified page.
fn url(
project_id: i64,
api_url: &str,
remote: &Remote,
ref_name: Option<&str>,
page: i32,
) -> String;
/// Returns the request buffer size.
fn buffer_size() -> usize;
/// Whether if exit early.
fn early_exit(&self) -> bool;
/// Returns the API URL for fetching the entries at the specified page.
fn url(
project_id: i64,
api_url: &str,
remote: &Remote,
ref_name: Option<&str>,
page: i32,
) -> String;
/// Returns the request buffer size.
fn buffer_size() -> usize;
/// Whether if exit early.
fn early_exit(&self) -> bool;
}
/// Trait for handling remote commits.
pub trait RemoteCommit: DynClone {
/// Commit SHA.
fn id(&self) -> String;
/// Commit author.
fn username(&self) -> Option<String>;
/// Timestamp.
fn timestamp(&self) -> Option<i64>;
/// Convert date in RFC3339 format to unix timestamp
fn convert_to_unix_timestamp(&self, date: &str) -> i64 {
OffsetDateTime::parse(date, &Rfc3339)
.expect("failed to parse date")
.unix_timestamp()
}
/// Commit SHA.
fn id(&self) -> String;
/// Commit author.
fn username(&self) -> Option<String>;
/// Timestamp.
fn timestamp(&self) -> Option<i64>;
/// Convert date in RFC3339 format to unix timestamp
fn convert_to_unix_timestamp(&self, date: &str) -> i64 {
OffsetDateTime::parse(date, &Rfc3339)
.expect("failed to parse date")
.unix_timestamp()
}
}
dyn_clone::clone_trait_object!(RemoteCommit);
/// Trait for handling remote pull requests.
pub trait RemotePullRequest: DynClone {
/// Number.
fn number(&self) -> i64;
/// Title.
fn title(&self) -> Option<String>;
/// Labels of the pull request.
fn labels(&self) -> Vec<String>;
/// Merge commit SHA.
fn merge_commit(&self) -> Option<String>;
/// Number.
fn number(&self) -> i64;
/// Title.
fn title(&self) -> Option<String>;
/// Labels of the pull request.
fn labels(&self) -> Vec<String>;
/// Merge commit SHA.
fn merge_commit(&self) -> Option<String>;
}
dyn_clone::clone_trait_object!(RemotePullRequest);
/// Result of a remote metadata.
pub type RemoteMetadata =
(Vec<Box<dyn RemoteCommit>>, Vec<Box<dyn RemotePullRequest>>);
pub type RemoteMetadata = (Vec<Box<dyn RemoteCommit>>, Vec<Box<dyn RemotePullRequest>>);
/// Metadata of a remote release.
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
pub struct RemoteReleaseMetadata {
/// Contributors.
pub contributors: Vec<RemoteContributor>,
/// Contributors.
pub contributors: Vec<RemoteContributor>,
}
impl Remote {
/// Creates a HTTP client for the remote.
fn create_client(&self, accept_header: &str) -> Result<ClientWithMiddleware> {
if !self.is_set() {
return Err(Error::RemoteNotSetError);
}
let mut headers = HeaderMap::new();
headers.insert(
reqwest::header::ACCEPT,
HeaderValue::from_str(accept_header)?,
);
if let Some(token) = &self.token {
headers.insert(
reqwest::header::AUTHORIZATION,
format!("Bearer {}", token.expose_secret()).parse()?,
);
}
headers.insert(reqwest::header::USER_AGENT, USER_AGENT.parse()?);
let client_builder = Client::builder()
.timeout(Duration::from_secs(REQUEST_TIMEOUT))
.tcp_keepalive(Duration::from_secs(REQUEST_KEEP_ALIVE))
.default_headers(headers)
.tls_built_in_root_certs(false);
let client_builder = if self.native_tls.unwrap_or(false) {
client_builder.tls_built_in_native_certs(true)
} else {
client_builder.tls_built_in_webpki_certs(true)
};
let client = client_builder.build()?;
let client = ClientBuilder::new(client)
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: CACacheManager {
path: dirs::cache_dir()
.ok_or_else(|| {
Error::DirsError(String::from(
"failed to find the user's cache directory",
))
})?
.join(env!("CARGO_PKG_NAME")),
},
options: HttpCacheOptions::default(),
}))
.build();
Ok(client)
}
/// Creates a HTTP client for the remote.
fn create_client(&self, accept_header: &str) -> Result<ClientWithMiddleware> {
if !self.is_set() {
return Err(Error::RemoteNotSetError);
}
let mut headers = HeaderMap::new();
headers.insert(
reqwest::header::ACCEPT,
HeaderValue::from_str(accept_header)?,
);
if let Some(token) = &self.token {
headers.insert(
reqwest::header::AUTHORIZATION,
format!("Bearer {}", token.expose_secret()).parse()?,
);
}
headers.insert(reqwest::header::USER_AGENT, USER_AGENT.parse()?);
let client_builder = Client::builder()
.timeout(Duration::from_secs(REQUEST_TIMEOUT))
.tcp_keepalive(Duration::from_secs(REQUEST_KEEP_ALIVE))
.default_headers(headers)
.tls_built_in_root_certs(false);
let client_builder = if self.native_tls.unwrap_or(false) {
client_builder.tls_built_in_native_certs(true)
} else {
client_builder.tls_built_in_webpki_certs(true)
};
let client = client_builder.build()?;
let client = ClientBuilder::new(client)
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: CACacheManager {
path: dirs::cache_dir()
.ok_or_else(|| {
Error::DirsError(String::from(
"failed to find the user's cache directory",
))
})?
.join(env!("CARGO_PKG_NAME")),
},
options: HttpCacheOptions::default(),
}))
.build();
Ok(client)
}
}
/// Trait for handling the API connection and fetching.
pub trait RemoteClient {
/// API URL for a particular client
const API_URL: &'static str;
/// API URL for a particular client
const API_URL: &'static str;
/// Name of the environment variable used to set the API URL to a
/// self-hosted instance (if applicable).
const API_URL_ENV: &'static str;
/// Name of the environment variable used to set the API URL to a
/// self-hosted instance (if applicable).
const API_URL_ENV: &'static str;
/// Returns the API url.
fn api_url(&self) -> String {
env::var(Self::API_URL_ENV)
.ok()
.or(self.remote().api_url)
.unwrap_or_else(|| Self::API_URL.to_string())
}
/// Returns the API url.
fn api_url(&self) -> String {
env::var(Self::API_URL_ENV)
.ok()
.or(self.remote().api_url)
.unwrap_or_else(|| Self::API_URL.to_string())
}
/// Returns the remote repository information.
fn remote(&self) -> Remote;
/// Returns the remote repository information.
fn remote(&self) -> Remote;
/// Returns the HTTP client for making requests.
fn client(&self) -> ClientWithMiddleware;
/// Returns the HTTP client for making requests.
fn client(&self) -> ClientWithMiddleware;
/// Returns true if the client should early exit.
fn early_exit<T: DeserializeOwned + RemoteEntry>(&self, page: &T) -> bool {
page.early_exit()
}
/// Returns true if the client should early exit.
fn early_exit<T: DeserializeOwned + RemoteEntry>(&self, page: &T) -> bool {
page.early_exit()
}
/// Retrieves a single object.
async fn get_entry<T: DeserializeOwned + RemoteEntry>(
&self,
project_id: i64,
ref_name: Option<&str>,
page: i32,
) -> Result<T> {
let url =
T::url(project_id, &self.api_url(), &self.remote(), ref_name, page);
debug!("Sending request to: {url}");
let response = self.client().get(&url).send().await?;
let response_text = if response.status().is_success() {
let text = response.text().await?;
trace!("Response: {:?}", text);
text
} else {
let text = response.text().await?;
error!("Request error: {}", text);
text
};
Ok(serde_json::from_str::<T>(&response_text)?)
}
/// Retrieves a single object.
async fn get_entry<T: DeserializeOwned + RemoteEntry>(
&self,
project_id: i64,
ref_name: Option<&str>,
page: i32,
) -> Result<T> {
let url = T::url(project_id, &self.api_url(), &self.remote(), ref_name, page);
debug!("Sending request to: {url}");
let response = self.client().get(&url).send().await?;
let response_text = if response.status().is_success() {
let text = response.text().await?;
trace!("Response: {:?}", text);
text
} else {
let text = response.text().await?;
error!("Request error: {}", text);
text
};
Ok(serde_json::from_str::<T>(&response_text)?)
}
/// Retrieves a single page of entries.
async fn get_entries_with_page<T: DeserializeOwned + RemoteEntry>(
&self,
project_id: i64,
ref_name: Option<&str>,
page: i32,
) -> Result<Vec<T>> {
let url =
T::url(project_id, &self.api_url(), &self.remote(), ref_name, page);
debug!("Sending request to: {url}");
let response = self.client().get(&url).send().await?;
let response_text = if response.status().is_success() {
let text = response.text().await?;
trace!("Response: {:?}", text);
text
} else {
let text = response.text().await?;
error!("Request error: {}", text);
text
};
let response = serde_json::from_str::<Vec<T>>(&response_text)?;
if response.is_empty() {
Err(Error::PaginationError(String::from("end of entries")))
} else {
Ok(response)
}
}
/// Retrieves a single page of entries.
async fn get_entries_with_page<T: DeserializeOwned + RemoteEntry>(
&self,
project_id: i64,
ref_name: Option<&str>,
page: i32,
) -> Result<Vec<T>> {
let url = T::url(project_id, &self.api_url(), &self.remote(), ref_name, page);
debug!("Sending request to: {url}");
let response = self.client().get(&url).send().await?;
let response_text = if response.status().is_success() {
let text = response.text().await?;
trace!("Response: {:?}", text);
text
} else {
let text = response.text().await?;
error!("Request error: {}", text);
text
};
let response = serde_json::from_str::<Vec<T>>(&response_text)?;
if response.is_empty() {
Err(Error::PaginationError(String::from("end of entries")))
} else {
Ok(response)
}
}
/// Fetches the remote API and returns the given entry.
///
/// See `fetch_with_early_exit` for the early exit version of this method.
async fn fetch<T: DeserializeOwned + RemoteEntry>(
&self,
project_id: i64,
ref_name: Option<&str>,
) -> Result<Vec<T>> {
let entries: Vec<Vec<T>> = stream::iter(0..)
.map(|i| self.get_entries_with_page(project_id, ref_name, i))
.buffered(T::buffer_size())
.take_while(|page| {
if let Err(e) = page {
debug!("Error while fetching page: {:?}", e);
}
future::ready(page.is_ok())
})
.map(|page| match page {
Ok(v) => v,
Err(ref e) => {
log::error!("{:#?}", e);
page.expect("failed to fetch page: {}")
}
})
.collect()
.await;
Ok(entries.into_iter().flatten().collect())
}
/// Fetches the remote API and returns the given entry.
///
/// See `fetch_with_early_exit` for the early exit version of this method.
async fn fetch<T: DeserializeOwned + RemoteEntry>(
&self,
project_id: i64,
ref_name: Option<&str>,
) -> Result<Vec<T>> {
let entries: Vec<Vec<T>> = stream::iter(0..)
.map(|i| self.get_entries_with_page(project_id, ref_name, i))
.buffered(T::buffer_size())
.take_while(|page| {
if let Err(e) = page {
debug!("Error while fetching page: {:?}", e);
}
future::ready(page.is_ok())
})
.map(|page| match page {
Ok(v) => v,
Err(ref e) => {
log::error!("{:#?}", e);
page.expect("failed to fetch page: {}")
}
})
.collect()
.await;
Ok(entries.into_iter().flatten().collect())
}
/// Fetches the remote API and returns the given entry.
///
/// Early exits based on the response.
async fn fetch_with_early_exit<T: DeserializeOwned + RemoteEntry>(
&self,
project_id: i64,
ref_name: Option<&str>,
) -> Result<Vec<T>> {
let entries: Vec<T> = stream::iter(0..)
.map(|i| self.get_entry::<T>(project_id, ref_name, i))
.buffered(T::buffer_size())
.take_while(|page| {
let status = match page {
Ok(v) => !self.early_exit(v),
Err(e) => {
debug!("Error while fetching page: {:?}", e);
true
}
};
future::ready(status && page.is_ok())
})
.map(|page| match page {
Ok(v) => v,
Err(ref e) => {
log::error!("{:#?}", e);
page.expect("failed to fetch page: {}")
}
})
.collect()
.await;
Ok(entries)
}
/// Fetches the remote API and returns the given entry.
///
/// Early exits based on the response.
async fn fetch_with_early_exit<T: DeserializeOwned + RemoteEntry>(
&self,
project_id: i64,
ref_name: Option<&str>,
) -> Result<Vec<T>> {
let entries: Vec<T> = stream::iter(0..)
.map(|i| self.get_entry::<T>(project_id, ref_name, i))
.buffered(T::buffer_size())
.take_while(|page| {
let status = match page {
Ok(v) => !self.early_exit(v),
Err(e) => {
debug!("Error while fetching page: {:?}", e);
true
}
};
future::ready(status && page.is_ok())
})
.map(|page| match page {
Ok(v) => v,
Err(ref e) => {
log::error!("{:#?}", e);
page.expect("failed to fetch page: {}")
}
})
.collect()
.await;
Ok(entries)
}
}
/// Generates a function for updating the release metadata for a remote.
#[doc(hidden)]
#[macro_export]
macro_rules! update_release_metadata {
($remote: ident, $fn: ident) => {
impl<'a> Release<'a> {
/// Updates the remote metadata that is contained in the release.
///
/// This function takes two arguments:
///
/// - Commits: needed for associating the Git user with the GitHub
/// username.
/// - Pull requests: needed for generating the contributor list for the
/// release.
#[allow(deprecated)]
pub fn $fn(
&mut self,
mut commits: Vec<Box<dyn RemoteCommit>>,
pull_requests: Vec<Box<dyn RemotePullRequest>>,
) -> Result<()> {
let mut contributors: Vec<RemoteContributor> = Vec::new();
let mut release_commit_timestamp: Option<i64> = None;
// retain the commits that are not a part of this release for later
// on checking the first contributors.
commits.retain(|v| {
if let Some(commit) =
self.commits.iter_mut().find(|commit| commit.id == v.id())
{
let sha_short =
Some(v.id().clone().chars().take(12).collect());
let pull_request = pull_requests.iter().find(|pr| {
pr.merge_commit() == Some(v.id().clone()) ||
pr.merge_commit() == sha_short
});
commit.$remote.username = v.username();
commit.$remote.pr_number = pull_request.map(|v| v.number());
commit.$remote.pr_title =
pull_request.and_then(|v| v.title().clone());
commit.$remote.pr_labels = pull_request
.map(|v| v.labels().clone())
.unwrap_or_default();
if !contributors
.iter()
.any(|v| commit.$remote.username == v.username)
{
contributors.push(RemoteContributor {
username: commit.$remote.username.clone(),
pr_title: commit.$remote.pr_title.clone(),
pr_number: commit.$remote.pr_number,
pr_labels: commit.$remote.pr_labels.clone(),
is_first_time: false,
});
}
commit.remote = Some(commit.$remote.clone());
// if remote commit is the release commit store timestamp for
// use in calculation of first time
if Some(v.id().clone()) == self.commit_id {
release_commit_timestamp = v.timestamp().clone();
}
false
} else {
true
}
});
// mark contributors as first-time
self.$remote.contributors = contributors
.into_iter()
.map(|mut v| {
v.is_first_time = !commits
.iter()
.filter(|commit| {
// if current release is unreleased no need to filter
// commits or filter commits that are from
// newer releases
self.timestamp == None ||
commit.timestamp() < release_commit_timestamp
})
.map(|v| v.username())
.any(|login| login == v.username);
v
})
.collect();
Ok(())
}
}
};
($remote: ident, $fn: ident) => {
impl<'a> Release<'a> {
/// Updates the remote metadata that is contained in the release.
///
/// This function takes two arguments:
///
/// - Commits: needed for associating the Git user with the GitHub username.
/// - Pull requests: needed for generating the contributor list for the release.
#[allow(deprecated)]
pub fn $fn(
&mut self,
mut commits: Vec<Box<dyn RemoteCommit>>,
pull_requests: Vec<Box<dyn RemotePullRequest>>,
) -> Result<()> {
let mut contributors: Vec<RemoteContributor> = Vec::new();
let mut release_commit_timestamp: Option<i64> = None;
// retain the commits that are not a part of this release for later
// on checking the first contributors.
commits.retain(|v| {
if let Some(commit) = self.commits.iter_mut().find(|commit| commit.id == v.id())
{
let sha_short = Some(v.id().clone().chars().take(12).collect());
let pull_request = pull_requests.iter().find(|pr| {
pr.merge_commit() == Some(v.id().clone()) ||
pr.merge_commit() == sha_short
});
commit.$remote.username = v.username();
commit.$remote.pr_number = pull_request.map(|v| v.number());
commit.$remote.pr_title = pull_request.and_then(|v| v.title().clone());
commit.$remote.pr_labels =
pull_request.map(|v| v.labels().clone()).unwrap_or_default();
if !contributors
.iter()
.any(|v| commit.$remote.username == v.username)
{
contributors.push(RemoteContributor {
username: commit.$remote.username.clone(),
pr_title: commit.$remote.pr_title.clone(),
pr_number: commit.$remote.pr_number,
pr_labels: commit.$remote.pr_labels.clone(),
is_first_time: false,
});
}
commit.remote = Some(commit.$remote.clone());
// if remote commit is the release commit store timestamp for
// use in calculation of first time
if Some(v.id().clone()) == self.commit_id {
release_commit_timestamp = v.timestamp().clone();
}
false
} else {
true
}
});
// mark contributors as first-time
self.$remote.contributors = contributors
.into_iter()
.map(|mut v| {
v.is_first_time = !commits
.iter()
.filter(|commit| {
// if current release is unreleased no need to filter
// commits or filter commits that are from
// newer releases
self.timestamp == None ||
commit.timestamp() < release_commit_timestamp
})
.map(|v| v.username())
.any(|login| login == v.username);
v
})
.collect();
Ok(())
}
}
};
}
+852 -931
View File
File diff suppressed because it is too large Load Diff
+265 -278
View File
@@ -10,301 +10,288 @@ use crate::release::Release;
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LinkCount {
/// Text of the link.
pub text: String,
/// URL of the link.
pub href: String,
/// The number of times this link was referenced.
pub count: usize,
/// Text of the link.
pub text: String,
/// URL of the link.
pub href: String,
/// The number of times this link was referenced.
pub count: usize,
}
/// Aggregated statistics about commits in the release.
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Statistics {
/// The total number of commits included in the release.
pub commit_count: usize,
/// The time span, in days, from the first to the last commit in the
/// release. Only present if there is more than one commit.
#[serde(skip_serializing_if = "Option::is_none")]
pub commits_timespan: Option<i64>,
/// The number of commits that follow the Conventional Commits
/// specification.
pub conventional_commit_count: usize,
/// The number of times each link was referenced in commit messages.
pub links: Vec<LinkCount>,
/// The number of days since the previous release.
/// Only present if this is not the first release.
#[serde(skip_serializing_if = "Option::is_none")]
pub days_passed_since_last_release: Option<i64>,
/// The total number of commits included in the release.
pub commit_count: usize,
/// The time span, in days, from the first to the last commit in the
/// release. Only present if there is more than one commit.
#[serde(skip_serializing_if = "Option::is_none")]
pub commits_timespan: Option<i64>,
/// The number of commits that follow the Conventional Commits
/// specification.
pub conventional_commit_count: usize,
/// The number of times each link was referenced in commit messages.
pub links: Vec<LinkCount>,
/// The number of days since the previous release.
/// Only present if this is not the first release.
#[serde(skip_serializing_if = "Option::is_none")]
pub days_passed_since_last_release: Option<i64>,
}
impl From<&Release<'_>> for Statistics {
/// Aggregates various statistics from the release data.
///
/// This method computes several metrics based on the current release and
/// its commits:
///
/// - Counts the total number of commits.
/// - Determines the number of days between the first and last commit.
/// - Counts the number of commits that follow the Conventional Commits
/// specification.
/// - Tallies how many times each link appears across all commit messages.
/// - Calculates the number of days since the previous release, if
/// available.
fn from(release: &Release) -> Self {
let commit_count = release.commits.len();
let commits_timespan = if release.commits.len() < 2 {
trace!(
"commits_timespan: insufficient commits to calculate duration \
(found {})",
release.commits.len()
);
None
} else {
release
.commits
.iter()
.min_by_key(|c| c.committer.timestamp)
.zip(release.commits.iter().max_by_key(|c| c.committer.timestamp))
.and_then(|(first, last)| {
Utc.timestamp_opt(first.committer.timestamp, 0)
.single()
.zip(Utc.timestamp_opt(last.committer.timestamp, 0).single())
.map(|(start, end)| {
(end.date_naive() - start.date_naive()).num_days()
})
})
};
let conventional_commit_count =
release.commits.iter().filter(|c| c.conv.is_some()).count();
let mut links: Vec<LinkCount> = release
.commits
.iter()
.fold(HashMap::new(), |mut acc, c| {
for link in &c.links {
*acc.entry((link.text.clone(), link.href.clone()))
.or_insert(0) += 1;
}
acc
})
.into_iter()
.map(|((text, href), count)| LinkCount { text, href, count })
.collect();
links.sort_by(|lhs, rhs| {
rhs.count
.cmp(&lhs.count)
.then_with(|| lhs.text.cmp(&rhs.text))
.then_with(|| lhs.href.cmp(&rhs.href))
});
let days_passed_since_last_release = match release.previous.as_ref() {
Some(prev) => release
.timestamp
.map(|ts| Utc.timestamp_opt(ts, 0))
.unwrap_or_else(|| {
let now = Utc::now();
Utc.timestamp_opt(now.timestamp(), 0)
})
.single()
.zip(
prev.timestamp
.and_then(|ts| Utc.timestamp_opt(ts, 0).single()),
)
.map(|(curr, prev)| {
(curr.date_naive() - prev.date_naive()).num_days()
}),
None => {
trace!("days_passed_since_last_release: previous release not found");
None
}
};
Self {
commit_count,
commits_timespan,
conventional_commit_count,
links,
days_passed_since_last_release,
}
}
/// Aggregates various statistics from the release data.
///
/// This method computes several metrics based on the current release and
/// its commits:
///
/// - Counts the total number of commits.
/// - Determines the number of days between the first and last commit.
/// - Counts the number of commits that follow the Conventional Commits specification.
/// - Tallies how many times each link appears across all commit messages.
/// - Calculates the number of days since the previous release, if available.
fn from(release: &Release) -> Self {
let commit_count = release.commits.len();
let commits_timespan = if release.commits.len() < 2 {
trace!(
"commits_timespan: insufficient commits to calculate duration (found {})",
release.commits.len()
);
None
} else {
release
.commits
.iter()
.min_by_key(|c| c.committer.timestamp)
.zip(release.commits.iter().max_by_key(|c| c.committer.timestamp))
.and_then(|(first, last)| {
Utc.timestamp_opt(first.committer.timestamp, 0)
.single()
.zip(Utc.timestamp_opt(last.committer.timestamp, 0).single())
.map(|(start, end)| (end.date_naive() - start.date_naive()).num_days())
})
};
let conventional_commit_count = release.commits.iter().filter(|c| c.conv.is_some()).count();
let mut links: Vec<LinkCount> = release
.commits
.iter()
.fold(HashMap::new(), |mut acc, c| {
for link in &c.links {
*acc.entry((link.text.clone(), link.href.clone()))
.or_insert(0) += 1;
}
acc
})
.into_iter()
.map(|((text, href), count)| LinkCount { text, href, count })
.collect();
links.sort_by(|lhs, rhs| {
rhs.count
.cmp(&lhs.count)
.then_with(|| lhs.text.cmp(&rhs.text))
.then_with(|| lhs.href.cmp(&rhs.href))
});
let days_passed_since_last_release = match release.previous.as_ref() {
Some(prev) => release
.timestamp
.map(|ts| Utc.timestamp_opt(ts, 0))
.unwrap_or_else(|| {
let now = Utc::now();
Utc.timestamp_opt(now.timestamp(), 0)
})
.single()
.zip(
prev.timestamp
.and_then(|ts| Utc.timestamp_opt(ts, 0).single()),
)
.map(|(curr, prev)| (curr.date_naive() - prev.date_naive()).num_days()),
None => {
trace!("days_passed_since_last_release: previous release not found");
None
}
};
Self {
commit_count,
commits_timespan,
conventional_commit_count,
links,
days_passed_since_last_release,
}
}
}
#[cfg(test)]
mod test {
use lazy_regex::Regex;
use pretty_assertions::assert_eq;
use lazy_regex::Regex;
use pretty_assertions::assert_eq;
use super::*;
use crate::commit::{Commit, Signature};
use crate::config::LinkParser;
use crate::error::Result;
use crate::release::Release;
#[test]
fn from_release() -> Result<()> {
fn find_count(v: &[LinkCount], text: &str, href: &str) -> Option<usize> {
v.iter()
.find(|l| l.text == text && l.href == href)
.map(|l| l.count)
}
let link_parsers = vec![
LinkParser {
pattern: Regex::new("RFC(\\d+)")?,
href: String::from("rfc://$1"),
text: None,
},
LinkParser {
pattern: Regex::new("#(\\d+)")?,
href: String::from("https://github.com/$1"),
text: None,
},
];
let unconventional_commits = vec![
Commit {
id: String::from("123123"),
message: String::from("add feature"),
committer: Signature {
name: Some(String::from("John Doe")),
email: Some(String::from("[email protected]")),
timestamp: 1649201111,
},
..Default::default()
},
Commit {
id: String::from("123123"),
message: String::from("fix feature"),
committer: Signature {
name: Some(String::from("John Doe")),
email: Some(String::from("[email protected]")),
timestamp: 1649201112,
},
..Default::default()
},
Commit {
id: String::from("123123"),
message: String::from("refactor feature"),
committer: Signature {
name: Some(String::from("John Doe")),
email: Some(String::from("[email protected]")),
timestamp: 1649201113,
},
..Default::default()
},
Commit {
id: String::from("123123"),
message: String::from("add docs for RFC456-related feature"),
committer: Signature {
name: Some(String::from("John Doe")),
email: Some(String::from("[email protected]")),
timestamp: 1649201114,
},
..Default::default()
},
];
let conventional_commits = vec![
Commit {
id: String::from("123123"),
message: String::from(
"perf: improve feature performance, fixes #455",
),
committer: Signature {
name: Some(String::from("John Doe")),
email: Some(String::from("[email protected]")),
timestamp: 1649287515,
},
..Default::default()
},
Commit {
id: String::from("123123"),
message: String::from("style(schema): fix feature schema"),
committer: Signature {
name: Some(String::from("John Doe")),
email: Some(String::from("[email protected]")),
timestamp: 1649287516,
},
..Default::default()
},
Commit {
id: String::from("123123"),
message: String::from(
"test: add unit tests for RFC456-related feature",
),
committer: Signature {
name: Some(String::from("John Doe")),
email: Some(String::from("[email protected]")),
timestamp: 1649287517,
},
..Default::default()
},
];
let commits: Vec<Commit> =
[unconventional_commits.clone(), conventional_commits.clone()]
.concat()
.into_iter()
.map(|c| c.parse_links(&link_parsers))
.map(|c| c.clone().into_conventional().unwrap_or(c))
.collect();
let release = Release {
commits,
timestamp: Some(1649373910),
previous: Some(Box::new(Release {
timestamp: Some(1649201110),
..Default::default()
})),
repository: Some(String::from("/root/repo")),
..Default::default()
};
use super::*;
use crate::commit::{Commit, Signature};
use crate::config::LinkParser;
use crate::error::Result;
use crate::release::Release;
#[test]
fn from_release() -> Result<()> {
fn find_count(v: &[LinkCount], text: &str, href: &str) -> Option<usize> {
v.iter()
.find(|l| l.text == text && l.href == href)
.map(|l| l.count)
}
let link_parsers = vec![
LinkParser {
pattern: Regex::new("RFC(\\d+)")?,
href: String::from("rfc://$1"),
text: None,
},
LinkParser {
pattern: Regex::new("#(\\d+)")?,
href: String::from("https://github.com/$1"),
text: None,
},
];
let unconventional_commits = vec![
Commit {
id: String::from("123123"),
message: String::from("add feature"),
committer: Signature {
name: Some(String::from("John Doe")),
email: Some(String::from("[email protected]")),
timestamp: 1649201111,
},
..Default::default()
},
Commit {
id: String::from("123123"),
message: String::from("fix feature"),
committer: Signature {
name: Some(String::from("John Doe")),
email: Some(String::from("[email protected]")),
timestamp: 1649201112,
},
..Default::default()
},
Commit {
id: String::from("123123"),
message: String::from("refactor feature"),
committer: Signature {
name: Some(String::from("John Doe")),
email: Some(String::from("[email protected]")),
timestamp: 1649201113,
},
..Default::default()
},
Commit {
id: String::from("123123"),
message: String::from("add docs for RFC456-related feature"),
committer: Signature {
name: Some(String::from("John Doe")),
email: Some(String::from("[email protected]")),
timestamp: 1649201114,
},
..Default::default()
},
];
let conventional_commits = vec![
Commit {
id: String::from("123123"),
message: String::from("perf: improve feature performance, fixes #455"),
committer: Signature {
name: Some(String::from("John Doe")),
email: Some(String::from("[email protected]")),
timestamp: 1649287515,
},
..Default::default()
},
Commit {
id: String::from("123123"),
message: String::from("style(schema): fix feature schema"),
committer: Signature {
name: Some(String::from("John Doe")),
email: Some(String::from("[email protected]")),
timestamp: 1649287516,
},
..Default::default()
},
Commit {
id: String::from("123123"),
message: String::from("test: add unit tests for RFC456-related feature"),
committer: Signature {
name: Some(String::from("John Doe")),
email: Some(String::from("[email protected]")),
timestamp: 1649287517,
},
..Default::default()
},
];
let commits: Vec<Commit> = [unconventional_commits.clone(), conventional_commits.clone()]
.concat()
.into_iter()
.map(|c| c.parse_links(&link_parsers))
.map(|c| c.clone().into_conventional().unwrap_or(c))
.collect();
let release = Release {
commits,
timestamp: Some(1649373910),
previous: Some(Box::new(Release {
timestamp: Some(1649201110),
..Default::default()
})),
repository: Some(String::from("/root/repo")),
..Default::default()
};
let statistics = Statistics::from(&release);
assert_eq!(release.commits.len(), statistics.commit_count);
assert_eq!(Some(1), statistics.commits_timespan);
assert_eq!(
conventional_commits.len(),
statistics.conventional_commit_count
);
assert_eq!(
Some(2),
find_count(&statistics.links, "RFC456", "rfc://456")
);
assert_eq!(
Some(1),
find_count(&statistics.links, "#455", "https://github.com/455")
);
assert_eq!(Some(2), statistics.days_passed_since_last_release);
let statistics = Statistics::from(&release);
assert_eq!(release.commits.len(), statistics.commit_count);
assert_eq!(Some(1), statistics.commits_timespan);
assert_eq!(
conventional_commits.len(),
statistics.conventional_commit_count
);
assert_eq!(
Some(2),
find_count(&statistics.links, "RFC456", "rfc://456")
);
assert_eq!(
Some(1),
find_count(&statistics.links, "#455", "https://github.com/455")
);
assert_eq!(Some(2), statistics.days_passed_since_last_release);
let commits = vec![Commit {
id: String::from("123123"),
message: String::from("add feature"),
committer: Signature {
name: Some(String::from("John Doe")),
email: Some(String::from("[email protected]")),
timestamp: 1649201111,
},
..Default::default()
}];
let release = Release {
commits,
timestamp: Some(1649373910),
previous: Some(Box::new(Release {
timestamp: Some(1649201110),
..Default::default()
})),
repository: Some(String::from("/root/repo")),
..Default::default()
};
let commits = vec![Commit {
id: String::from("123123"),
message: String::from("add feature"),
committer: Signature {
name: Some(String::from("John Doe")),
email: Some(String::from("[email protected]")),
timestamp: 1649201111,
},
..Default::default()
}];
let release = Release {
commits,
timestamp: Some(1649373910),
previous: Some(Box::new(Release {
timestamp: Some(1649201110),
..Default::default()
})),
repository: Some(String::from("/root/repo")),
..Default::default()
};
let statistics = Statistics::from(&release);
assert_eq!(None, statistics.commits_timespan);
let statistics = Statistics::from(&release);
assert_eq!(None, statistics.commits_timespan);
let commits = vec![];
let release = Release {
commits,
timestamp: Some(1649373910),
previous: None,
repository: Some(String::from("/root/repo")),
..Default::default()
};
let commits = vec![];
let release = Release {
commits,
timestamp: Some(1649373910),
previous: None,
repository: Some(String::from("/root/repo")),
..Default::default()
};
let statistics = Statistics::from(&release);
assert_eq!(None, statistics.days_passed_since_last_release);
let statistics = Statistics::from(&release);
assert_eq!(None, statistics.days_passed_since_last_release);
Ok(())
}
Ok(())
}
}
+34 -34
View File
@@ -3,45 +3,45 @@
/// Lightweight tags will have `None` as message.
#[derive(Debug)]
pub struct Tag {
/// The name of the tag
pub name: String,
/// The message of the tag (only if it was annotated).
pub message: Option<String>,
/// The name of the tag
pub name: String,
/// The message of the tag (only if it was annotated).
pub message: Option<String>,
}
#[cfg(test)]
mod test {
use super::*;
use super::*;
#[test]
fn create_tag_with_name_and_message() {
let tag = Tag {
name: String::from("v1.0"),
message: Some(String::from("Initial release")),
};
assert_eq!(tag.name, "v1.0");
assert_eq!(tag.message, Some(String::from("Initial release")));
}
#[test]
fn create_tag_with_name_and_message() {
let tag = Tag {
name: String::from("v1.0"),
message: Some(String::from("Initial release")),
};
assert_eq!(tag.name, "v1.0");
assert_eq!(tag.message, Some(String::from("Initial release")));
}
#[test]
fn create_tag_with_name_and_no_message() {
let tag = Tag {
name: String::from("v1.0"),
message: None,
};
assert_eq!(tag.name, "v1.0");
assert_eq!(tag.message, None);
}
#[test]
fn create_tag_with_name_and_no_message() {
let tag = Tag {
name: String::from("v1.0"),
message: None,
};
assert_eq!(tag.name, "v1.0");
assert_eq!(tag.message, None);
}
#[test]
fn debug_print_tag_with_message() {
let tag = Tag {
name: String::from("v1.0"),
message: Some(String::from("Initial release")),
};
assert_eq!(
format!("{:?}", tag),
"Tag { name: \"v1.0\", message: Some(\"Initial release\") }"
);
}
#[test]
fn debug_print_tag_with_message() {
let tag = Tag {
name: String::from("v1.0"),
message: Some(String::from("Initial release")),
};
assert_eq!(
format!("{:?}", tag),
"Tag { name: \"v1.0\", message: Some(\"Initial release\") }"
);
}
}
+247 -269
View File
@@ -10,297 +10,275 @@ use crate::error::{Error, Result};
/// Wrapper for [`Tera`].
#[derive(Debug)]
pub struct Template {
/// Template name.
name: String,
/// Internal Tera instance.
tera: Tera,
/// Template variables.
#[cfg_attr(not(feature = "github"), allow(dead_code))]
pub variables: Vec<String>,
/// Template name.
name: String,
/// Internal Tera instance.
tera: Tera,
/// Template variables.
#[cfg_attr(not(feature = "github"), allow(dead_code))]
pub variables: Vec<String>,
}
impl Template {
/// Constructs a new instance.
pub fn new(name: &str, mut content: String, trim: bool) -> Result<Self> {
if trim {
content = content
.lines()
.map(|v| v.trim())
.collect::<Vec<&str>>()
.join("\n");
}
let mut tera = Tera::default();
if let Err(e) = tera.add_raw_template(name, &content) {
return if let Some(error_source) = e.source() {
Err(Error::TemplateParseError(error_source.to_string()))
} else {
Err(Error::TemplateError(e))
};
}
tera.register_filter("upper_first", Self::upper_first_filter);
Ok(Self {
name: name.to_string(),
variables: Self::get_template_variables(name, &tera)?,
tera,
})
}
/// Constructs a new instance.
pub fn new(name: &str, mut content: String, trim: bool) -> Result<Self> {
if trim {
content = content
.lines()
.map(|v| v.trim())
.collect::<Vec<&str>>()
.join("\n");
}
let mut tera = Tera::default();
if let Err(e) = tera.add_raw_template(name, &content) {
return if let Some(error_source) = e.source() {
Err(Error::TemplateParseError(error_source.to_string()))
} else {
Err(Error::TemplateError(e))
};
}
tera.register_filter("upper_first", Self::upper_first_filter);
Ok(Self {
name: name.to_string(),
variables: Self::get_template_variables(name, &tera)?,
tera,
})
}
/// Filter for making the first character of a string uppercase.
fn upper_first_filter(
value: &Value,
_: &HashMap<String, Value>,
) -> TeraResult<Value> {
let mut s =
tera::try_get_value!("upper_first_filter", "value", String, value);
let mut c = s.chars();
s = match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
};
Ok(tera::to_value(&s)?)
}
/// Filter for making the first character of a string uppercase.
fn upper_first_filter(value: &Value, _: &HashMap<String, Value>) -> TeraResult<Value> {
let mut s = tera::try_get_value!("upper_first_filter", "value", String, value);
let mut c = s.chars();
s = match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
};
Ok(tera::to_value(&s)?)
}
/// Recursively finds the identifiers from the AST.
fn find_identifiers(node: &ast::Node, names: &mut HashSet<String>) {
match node {
ast::Node::Block(_, block, _) => {
for node in &block.body {
Self::find_identifiers(node, names);
}
}
ast::Node::VariableBlock(_, expr) => {
if let ast::ExprVal::Ident(v) = &expr.val {
names.insert(v.clone());
}
}
ast::Node::MacroDefinition(_, def, _) => {
for node in &def.body {
Self::find_identifiers(node, names);
}
}
ast::Node::FilterSection(_, section, _) => {
for node in &section.body {
Self::find_identifiers(node, names);
}
}
ast::Node::Forloop(_, forloop, _) => {
if let ast::ExprVal::Ident(v) = &forloop.container.val {
names.insert(v.clone());
}
for node in &forloop.body {
Self::find_identifiers(node, names);
}
for node in &forloop.empty_body.clone().unwrap_or_default() {
Self::find_identifiers(node, names);
}
for (_, expr) in
forloop.container.filters.iter().flat_map(|v| v.args.iter())
{
if let ast::ExprVal::String(ref v) = expr.val {
names.insert(v.clone());
}
}
}
ast::Node::If(cond, _) => {
for (_, expr, nodes) in &cond.conditions {
if let ast::ExprVal::Ident(v) = &expr.val {
names.insert(v.clone());
}
for node in nodes {
Self::find_identifiers(node, names);
}
}
if let Some((_, nodes)) = &cond.otherwise {
for node in nodes {
Self::find_identifiers(node, names);
}
}
}
_ => {}
}
}
/// Recursively finds the identifiers from the AST.
fn find_identifiers(node: &ast::Node, names: &mut HashSet<String>) {
match node {
ast::Node::Block(_, block, _) => {
for node in &block.body {
Self::find_identifiers(node, names);
}
}
ast::Node::VariableBlock(_, expr) => {
if let ast::ExprVal::Ident(v) = &expr.val {
names.insert(v.clone());
}
}
ast::Node::MacroDefinition(_, def, _) => {
for node in &def.body {
Self::find_identifiers(node, names);
}
}
ast::Node::FilterSection(_, section, _) => {
for node in &section.body {
Self::find_identifiers(node, names);
}
}
ast::Node::Forloop(_, forloop, _) => {
if let ast::ExprVal::Ident(v) = &forloop.container.val {
names.insert(v.clone());
}
for node in &forloop.body {
Self::find_identifiers(node, names);
}
for node in &forloop.empty_body.clone().unwrap_or_default() {
Self::find_identifiers(node, names);
}
for (_, expr) in forloop.container.filters.iter().flat_map(|v| v.args.iter()) {
if let ast::ExprVal::String(ref v) = expr.val {
names.insert(v.clone());
}
}
}
ast::Node::If(cond, _) => {
for (_, expr, nodes) in &cond.conditions {
if let ast::ExprVal::Ident(v) = &expr.val {
names.insert(v.clone());
}
for node in nodes {
Self::find_identifiers(node, names);
}
}
if let Some((_, nodes)) = &cond.otherwise {
for node in nodes {
Self::find_identifiers(node, names);
}
}
}
_ => {}
}
}
/// Returns the variable names that are used in the template.
fn get_template_variables(name: &str, tera: &Tera) -> Result<Vec<String>> {
let mut variables = HashSet::new();
let ast = &tera.get_template(name)?.ast;
for node in ast {
Self::find_identifiers(node, &mut variables);
}
trace!("Template variables for {name}: {variables:?}");
Ok(variables.into_iter().collect())
}
/// Returns the variable names that are used in the template.
fn get_template_variables(name: &str, tera: &Tera) -> Result<Vec<String>> {
let mut variables = HashSet::new();
let ast = &tera.get_template(name)?.ast;
for node in ast {
Self::find_identifiers(node, &mut variables);
}
trace!("Template variables for {name}: {variables:?}");
Ok(variables.into_iter().collect())
}
/// Returns `true` if the template contains one of the given variables.
pub(crate) fn contains_variable(&self, variables: &[&str]) -> bool {
variables
.iter()
.any(|var| self.variables.iter().any(|v| v.starts_with(var)))
}
/// Returns `true` if the template contains one of the given variables.
pub(crate) fn contains_variable(&self, variables: &[&str]) -> bool {
variables
.iter()
.any(|var| self.variables.iter().any(|v| v.starts_with(var)))
}
/// Renders the template.
pub fn render<C: Serialize, T: Serialize, S: Into<String> + Clone>(
&self,
context: &C,
additional_context: Option<&HashMap<S, T>>,
postprocessors: &[TextProcessor],
) -> Result<String> {
let mut context = TeraContext::from_serialize(context)?;
if let Some(additional_context) = additional_context {
for (key, value) in additional_context {
context.insert(key.clone(), &value);
}
}
match self.tera.render(&self.name, &context) {
Ok(mut v) => {
for postprocessor in postprocessors {
postprocessor.replace(&mut v, vec![])?;
}
Ok(v)
}
Err(e) => {
if let Some(source1) = e.source() {
if let Some(source2) = source1.source() {
Err(Error::TemplateRenderDetailedError(
source1.to_string(),
source2.to_string(),
))
} else {
Err(Error::TemplateRenderError(source1.to_string()))
}
} else {
Err(Error::TemplateError(e))
}
}
}
}
/// Renders the template.
pub fn render<C: Serialize, T: Serialize, S: Into<String> + Clone>(
&self,
context: &C,
additional_context: Option<&HashMap<S, T>>,
postprocessors: &[TextProcessor],
) -> Result<String> {
let mut context = TeraContext::from_serialize(context)?;
if let Some(additional_context) = additional_context {
for (key, value) in additional_context {
context.insert(key.clone(), &value);
}
}
match self.tera.render(&self.name, &context) {
Ok(mut v) => {
for postprocessor in postprocessors {
postprocessor.replace(&mut v, vec![])?;
}
Ok(v)
}
Err(e) => {
if let Some(source1) = e.source() {
if let Some(source2) = source1.source() {
Err(Error::TemplateRenderDetailedError(
source1.to_string(),
source2.to_string(),
))
} else {
Err(Error::TemplateRenderError(source1.to_string()))
}
} else {
Err(Error::TemplateError(e))
}
}
}
}
}
#[cfg(test)]
mod test {
use regex::Regex;
use regex::Regex;
use super::*;
use crate::commit::Commit;
use crate::release::Release;
use super::*;
use crate::commit::Commit;
use crate::release::Release;
fn get_fake_release_data() -> Release<'static> {
Release {
version: Some(String::from("1.0")),
message: None,
extra: None,
commits: vec![
Commit::new(
String::from("123123"),
String::from("feat(xyz): add xyz"),
),
Commit::new(
String::from("124124"),
String::from("fix(abc): fix abc"),
),
]
.into_iter()
.filter_map(|c| c.into_conventional().ok())
.collect(),
commit_range: None,
commit_id: None,
timestamp: None,
previous: None,
repository: Some(String::from("/root/repo")),
submodule_commits: HashMap::new(),
statistics: None,
#[cfg(feature = "github")]
github: crate::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "gitlab")]
gitlab: crate::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "gitea")]
gitea: crate::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "bitbucket")]
bitbucket: crate::remote::RemoteReleaseMetadata {
contributors: vec![],
},
}
}
fn get_fake_release_data() -> Release<'static> {
Release {
version: Some(String::from("1.0")),
message: None,
extra: None,
commits: vec![
Commit::new(String::from("123123"), String::from("feat(xyz): add xyz")),
Commit::new(String::from("124124"), String::from("fix(abc): fix abc")),
]
.into_iter()
.filter_map(|c| c.into_conventional().ok())
.collect(),
commit_range: None,
commit_id: None,
timestamp: None,
previous: None,
repository: Some(String::from("/root/repo")),
submodule_commits: HashMap::new(),
statistics: None,
#[cfg(feature = "github")]
github: crate::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "gitlab")]
gitlab: crate::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "gitea")]
gitea: crate::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "bitbucket")]
bitbucket: crate::remote::RemoteReleaseMetadata {
contributors: vec![],
},
}
}
#[test]
fn render_template() -> Result<()> {
let template = r#"
#[test]
fn render_template() -> Result<()> {
let template = r#"
## {{ version }} - <DATE>
{% for commit in commits %}
### {{ commit.group }}
- {{ commit.message | upper_first }}
{% endfor %}"#;
let mut template = Template::new("test", template.to_string(), false)?;
let release = get_fake_release_data();
assert_eq!(
"\n\t\t## 1.0 - 2023\n\t\t\n\t\t### feat\n\t\t- Add xyz\n\t\t\n\t\t### \
fix\n\t\t- Fix abc\n\t\t",
template.render(
&release,
Option::<HashMap<&str, String>>::None.as_ref(),
&[TextProcessor {
pattern: Regex::new("<DATE>")
.expect("failed to compile regex"),
replace: Some(String::from("2023")),
replace_command: None,
}],
)?
);
template.variables.sort();
assert_eq!(
vec![
String::from("commit.group"),
String::from("commit.message"),
String::from("commits"),
String::from("version"),
],
template.variables
);
#[cfg(feature = "github")]
{
assert!(!template.contains_variable(&["commit.github"]));
assert!(template.contains_variable(&["commit.group"]));
}
Ok(())
}
let mut template = Template::new("test", template.to_string(), false)?;
let release = get_fake_release_data();
assert_eq!(
"\n\t\t## 1.0 - 2023\n\t\t\n\t\t### feat\n\t\t- Add xyz\n\t\t\n\t\t### fix\n\t\t- Fix \
abc\n\t\t",
template.render(&release, Option::<HashMap<&str, String>>::None.as_ref(), &[
TextProcessor {
pattern: Regex::new("<DATE>").expect("failed to compile regex"),
replace: Some(String::from("2023")),
replace_command: None,
}
],)?
);
template.variables.sort();
assert_eq!(
vec![
String::from("commit.group"),
String::from("commit.message"),
String::from("commits"),
String::from("version"),
],
template.variables
);
#[cfg(feature = "github")]
{
assert!(!template.contains_variable(&["commit.github"]));
assert!(template.contains_variable(&["commit.group"]));
}
Ok(())
}
#[test]
fn render_trimmed_template() -> Result<()> {
let template = r#"
#[test]
fn render_trimmed_template() -> Result<()> {
let template = r#"
## {{ version }}
"#;
let template = Template::new("test", template.to_string(), true)?;
let release = get_fake_release_data();
assert_eq!(
"\n## 1.0\n",
template.render(
&release,
Option::<HashMap<&str, String>>::None.as_ref(),
&[],
)?
);
assert_eq!(vec![String::from("version"),], template.variables);
Ok(())
}
let template = Template::new("test", template.to_string(), true)?;
let release = get_fake_release_data();
assert_eq!(
"\n## 1.0\n",
template.render(&release, Option::<HashMap<&str, String>>::None.as_ref(), &[
],)?
);
assert_eq!(vec![String::from("version"),], template.variables);
Ok(())
}
#[test]
fn test_upper_first_filter() -> Result<()> {
let template =
"{% set hello_variable = 'hello' %}{{ hello_variable | upper_first }}";
let release = get_fake_release_data();
let template = Template::new("test", template.to_string(), true)?;
let r = template.render(
&release,
Option::<HashMap<&str, String>>::None.as_ref(),
&[],
)?;
assert_eq!("Hello", r);
Ok(())
}
#[test]
fn test_upper_first_filter() -> Result<()> {
let template = "{% set hello_variable = 'hello' %}{{ hello_variable | upper_first }}";
let release = get_fake_release_data();
let template = Template::new("test", template.to_string(), true)?;
let r = template.render(&release, Option::<HashMap<&str, String>>::None.as_ref(), &[
])?;
assert_eq!("Hello", r);
Ok(())
}
}
+227 -229
View File
@@ -2,9 +2,7 @@ use std::collections::HashMap;
use std::fmt::Write;
use git_cliff_core::commit::{Commit, Range, Signature};
use git_cliff_core::config::{
ChangelogConfig, CommitParser, GitConfig, LinkParser, TextProcessor,
};
use git_cliff_core::config::{ChangelogConfig, CommitParser, GitConfig, LinkParser, TextProcessor};
use git_cliff_core::error::Result;
use git_cliff_core::release::*;
use git_cliff_core::template::Template;
@@ -13,10 +11,10 @@ use regex::Regex;
#[test]
fn generate_changelog() -> Result<()> {
let changelog_config = ChangelogConfig {
header: Some(String::from("this is a changelog")),
body: String::from(
r#"
let changelog_config = ChangelogConfig {
header: Some(String::from("this is a changelog")),
body: String::from(
r#"
## Release {{ version }} - <DATE>
{{ commit_range.from }}..{{ commit_range.to }}
@@ -33,143 +31,143 @@ fn generate_changelog() -> Result<()> {
{% endif -%}
{% endfor -%}
{% endfor %}"#,
),
footer: Some(String::from("eoc - end of changelog")),
trim: true,
render_always: false,
postprocessors: [].to_vec(),
output: None,
};
let git_config = GitConfig {
conventional_commits: true,
require_conventional: false,
filter_unconventional: true,
split_commits: false,
commit_preprocessors: vec![TextProcessor {
pattern: Regex::new(r"\(fixes (#[1-9]+)\)").unwrap(),
replace: Some(String::from("[closes Issue${1}]")),
replace_command: None,
}],
commit_parsers: vec![
CommitParser {
sha: Some(String::from("coffee")),
message: None,
body: None,
footer: None,
group: Some(String::from("I love coffee")),
default_scope: None,
scope: None,
skip: None,
field: None,
pattern: None,
},
CommitParser {
sha: None,
message: Regex::new("^feat").ok(),
body: None,
footer: None,
group: Some(String::from("shiny features")),
default_scope: None,
scope: None,
skip: None,
field: None,
pattern: None,
},
CommitParser {
sha: None,
message: Regex::new("^fix").ok(),
body: None,
footer: None,
group: Some(String::from("fix bugs")),
default_scope: None,
scope: None,
skip: None,
field: None,
pattern: None,
},
CommitParser {
sha: None,
message: Regex::new("^test").ok(),
body: None,
footer: None,
group: None,
default_scope: None,
scope: Some(String::from("tests")),
skip: None,
field: None,
pattern: None,
},
CommitParser {
sha: None,
message: None,
body: None,
footer: None,
group: Some(String::from("docs")),
default_scope: None,
scope: None,
skip: None,
field: Some(String::from("author.name")),
pattern: Regex::new("John Doe").ok(),
},
],
protect_breaking_commits: false,
filter_commits: true,
tag_pattern: None,
skip_tags: None,
ignore_tags: None,
count_tags: None,
use_branch_tags: false,
topo_order: false,
topo_order_commits: true,
sort_commits: String::from("oldest"),
link_parsers: vec![
LinkParser {
pattern: Regex::new("#(\\d+)").unwrap(),
href: String::from("https://github.com/$1"),
text: None,
},
LinkParser {
pattern: Regex::new("https://github.com/(.*)").unwrap(),
href: String::from("https://github.com/$1"),
text: Some(String::from("$1")),
},
],
limit_commits: None,
recurse_submodules: None,
include_paths: Vec::new(),
exclude_paths: Vec::new(),
};
),
footer: Some(String::from("eoc - end of changelog")),
trim: true,
render_always: false,
postprocessors: [].to_vec(),
output: None,
};
let git_config = GitConfig {
conventional_commits: true,
require_conventional: false,
filter_unconventional: true,
split_commits: false,
commit_preprocessors: vec![TextProcessor {
pattern: Regex::new(r"\(fixes (#[1-9]+)\)").unwrap(),
replace: Some(String::from("[closes Issue${1}]")),
replace_command: None,
}],
commit_parsers: vec![
CommitParser {
sha: Some(String::from("coffee")),
message: None,
body: None,
footer: None,
group: Some(String::from("I love coffee")),
default_scope: None,
scope: None,
skip: None,
field: None,
pattern: None,
},
CommitParser {
sha: None,
message: Regex::new("^feat").ok(),
body: None,
footer: None,
group: Some(String::from("shiny features")),
default_scope: None,
scope: None,
skip: None,
field: None,
pattern: None,
},
CommitParser {
sha: None,
message: Regex::new("^fix").ok(),
body: None,
footer: None,
group: Some(String::from("fix bugs")),
default_scope: None,
scope: None,
skip: None,
field: None,
pattern: None,
},
CommitParser {
sha: None,
message: Regex::new("^test").ok(),
body: None,
footer: None,
group: None,
default_scope: None,
scope: Some(String::from("tests")),
skip: None,
field: None,
pattern: None,
},
CommitParser {
sha: None,
message: None,
body: None,
footer: None,
group: Some(String::from("docs")),
default_scope: None,
scope: None,
skip: None,
field: Some(String::from("author.name")),
pattern: Regex::new("John Doe").ok(),
},
],
protect_breaking_commits: false,
filter_commits: true,
tag_pattern: None,
skip_tags: None,
ignore_tags: None,
count_tags: None,
use_branch_tags: false,
topo_order: false,
topo_order_commits: true,
sort_commits: String::from("oldest"),
link_parsers: vec![
LinkParser {
pattern: Regex::new("#(\\d+)").unwrap(),
href: String::from("https://github.com/$1"),
text: None,
},
LinkParser {
pattern: Regex::new("https://github.com/(.*)").unwrap(),
href: String::from("https://github.com/$1"),
text: Some(String::from("$1")),
},
],
limit_commits: None,
recurse_submodules: None,
include_paths: Vec::new(),
exclude_paths: Vec::new(),
};
let mut commit_with_author = Commit::new(
String::from("hjdfas32"),
String::from("docs(cool): testing author filtering"),
);
let mut commit_with_author = Commit::new(
String::from("hjdfas32"),
String::from("docs(cool): testing author filtering"),
);
commit_with_author.author = Signature {
name: Some("John Doe".to_string()),
email: None,
timestamp: 0x0,
};
commit_with_author.author = Signature {
name: Some("John Doe".to_string()),
email: None,
timestamp: 0x0,
};
let release_v1_commits = vec![
Commit::new(
String::from("0bc123"),
String::from("feat: add cool features"),
),
Commit::new(String::from("0werty"), String::from("fix: fix stuff")),
Commit::new(String::from("0w3rty"), String::from("fix: fix more stuff")),
Commit::new(String::from("0jkl12"), String::from("chore: do nothing")),
]
.into_iter()
.filter_map(|c| c.into_conventional().ok())
.collect::<Vec<Commit>>();
let release_v1_commits = vec![
Commit::new(
String::from("0bc123"),
String::from("feat: add cool features"),
),
Commit::new(String::from("0werty"), String::from("fix: fix stuff")),
Commit::new(String::from("0w3rty"), String::from("fix: fix more stuff")),
Commit::new(String::from("0jkl12"), String::from("chore: do nothing")),
]
.into_iter()
.filter_map(|c| c.into_conventional().ok())
.collect::<Vec<Commit>>();
let release_v1_commit_range = Range::new(
release_v1_commits.first().unwrap(),
release_v1_commits.last().unwrap(),
);
let release_v1_commit_range = Range::new(
release_v1_commits.first().unwrap(),
release_v1_commits.last().unwrap(),
);
let release_v2_commits = vec![
let release_v2_commits = vec![
Commit::new(
String::from("000abc"),
String::from("Add unconventional commit"),
@@ -210,97 +208,97 @@ fn generate_changelog() -> Result<()> {
.filter_map(|c| c.process(&git_config).ok())
.collect::<Vec<Commit>>();
let release_v2_commit_range = Range::new(
release_v2_commits.first().unwrap(),
release_v2_commits.last().unwrap(),
);
let release_v2_commit_range = Range::new(
release_v2_commits.first().unwrap(),
release_v2_commits.last().unwrap(),
);
let releases = vec![
Release {
version: Some(String::from("v2.0.0")),
message: None,
extra: None,
commits: release_v2_commits,
commit_range: Some(release_v2_commit_range),
commit_id: None,
timestamp: None,
previous: None,
repository: Some(String::from("/root/repo")),
submodule_commits: HashMap::new(),
statistics: None,
#[cfg(feature = "github")]
github: git_cliff_core::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "gitlab")]
gitlab: git_cliff_core::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "gitea")]
gitea: git_cliff_core::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "bitbucket")]
bitbucket: git_cliff_core::remote::RemoteReleaseMetadata {
contributors: vec![],
},
},
Release {
version: Some(String::from("v1.0.0")),
message: None,
extra: None,
commits: release_v1_commits,
commit_range: Some(release_v1_commit_range),
commit_id: None,
timestamp: None,
previous: None,
repository: Some(String::from("/root/repo")),
submodule_commits: HashMap::new(),
statistics: None,
#[cfg(feature = "github")]
github: git_cliff_core::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "gitlab")]
gitlab: git_cliff_core::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "gitea")]
gitea: git_cliff_core::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "bitbucket")]
bitbucket: git_cliff_core::remote::RemoteReleaseMetadata {
contributors: vec![],
},
},
];
let releases = vec![
Release {
version: Some(String::from("v2.0.0")),
message: None,
extra: None,
commits: release_v2_commits,
commit_range: Some(release_v2_commit_range),
commit_id: None,
timestamp: None,
previous: None,
repository: Some(String::from("/root/repo")),
submodule_commits: HashMap::new(),
statistics: None,
#[cfg(feature = "github")]
github: git_cliff_core::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "gitlab")]
gitlab: git_cliff_core::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "gitea")]
gitea: git_cliff_core::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "bitbucket")]
bitbucket: git_cliff_core::remote::RemoteReleaseMetadata {
contributors: vec![],
},
},
Release {
version: Some(String::from("v1.0.0")),
message: None,
extra: None,
commits: release_v1_commits,
commit_range: Some(release_v1_commit_range),
commit_id: None,
timestamp: None,
previous: None,
repository: Some(String::from("/root/repo")),
submodule_commits: HashMap::new(),
statistics: None,
#[cfg(feature = "github")]
github: git_cliff_core::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "gitlab")]
gitlab: git_cliff_core::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "gitea")]
gitea: git_cliff_core::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "bitbucket")]
bitbucket: git_cliff_core::remote::RemoteReleaseMetadata {
contributors: vec![],
},
},
];
let out = &mut String::new();
let template = Template::new("test", changelog_config.body, false)?;
let out = &mut String::new();
let template = Template::new("test", changelog_config.body, false)?;
writeln!(out, "{}", changelog_config.header.unwrap()).unwrap();
let text_processors = [TextProcessor {
pattern: Regex::new("<DATE>").unwrap(),
replace: Some(String::from("2023")),
replace_command: None,
}];
for release in releases {
write!(
out,
"{}",
template.render(
&release,
Option::<HashMap<&str, String>>::None.as_ref(),
&text_processors
)?
)
.unwrap();
}
writeln!(out, "{}", changelog_config.footer.unwrap()).unwrap();
writeln!(out, "{}", changelog_config.header.unwrap()).unwrap();
let text_processors = [TextProcessor {
pattern: Regex::new("<DATE>").unwrap(),
replace: Some(String::from("2023")),
replace_command: None,
}];
for release in releases {
write!(
out,
"{}",
template.render(
&release,
Option::<HashMap<&str, String>>::None.as_ref(),
&text_processors
)?
)
.unwrap();
}
writeln!(out, "{}", changelog_config.footer.unwrap()).unwrap();
assert_eq!(
r#"this is a changelog
assert_eq!(
r#"this is a changelog
## Release v2.0.0 - 2023
@@ -338,8 +336,8 @@ abc123..hjdfas32
- fix more stuff
eoc - end of changelog
"#,
out
);
out
);
Ok(())
Ok(())
}
+3 -3
View File
@@ -3,7 +3,7 @@ use git_cliff::args::Opt;
use git_cliff_core::error::Result;
fn main() -> Result<()> {
let args = Opt::parse();
git_cliff::run(args)?;
Ok(())
let args = Opt::parse();
git_cliff::run(args)?;
Ok(())
}
+301 -309
View File
@@ -13,22 +13,22 @@ use url::Url;
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum Strip {
Header,
Footer,
All,
Header,
Footer,
All,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum Sort {
Oldest,
Newest,
Oldest,
Newest,
}
const STYLES: Styles = Styles::styled()
.header(Ansi256Color(208).on_default().bold())
.usage(Ansi256Color(208).on_default().bold())
.literal(AnsiColor::White.on_default())
.placeholder(AnsiColor::Green.on_default());
.header(Ansi256Color(208).on_default().bold())
.usage(Ansi256Color(208).on_default().bold())
.literal(AnsiColor::White.on_default())
.placeholder(AnsiColor::Green.on_default());
/// Command-line arguments to parse.
#[derive(Debug, Parser)]
@@ -52,7 +52,7 @@ const STYLES: Styles = Styles::styled()
styles(STYLES),
)]
pub struct Opt {
#[arg(
#[arg(
short,
long,
action = ArgAction::Help,
@@ -60,8 +60,8 @@ pub struct Opt {
help = "Prints help information",
help_heading = "FLAGS"
)]
pub help: Option<bool>,
#[arg(
pub help: Option<bool>,
#[arg(
short = 'V',
long,
action = ArgAction::Version,
@@ -69,21 +69,21 @@ pub struct Opt {
help = "Prints version information",
help_heading = "FLAGS"
)]
pub version: Option<bool>,
/// Increases the logging verbosity.
#[arg(short, long, action = ArgAction::Count, alias = "debug", help_heading = Some("FLAGS"))]
pub verbose: u8,
/// Writes the default configuration file to cliff.toml
#[arg(
pub version: Option<bool>,
/// Increases the logging verbosity.
#[arg(short, long, action = ArgAction::Count, alias = "debug", help_heading = Some("FLAGS"))]
pub verbose: u8,
/// Writes the default configuration file to cliff.toml
#[arg(
short,
long,
value_name = "CONFIG",
num_args = 0..=1,
required = false
)]
pub init: Option<Option<String>>,
/// Sets the configuration file.
#[arg(
pub init: Option<Option<String>>,
/// Sets the configuration file.
#[arg(
short,
long,
env = "GIT_CLIFF_CONFIG",
@@ -91,21 +91,21 @@ pub struct Opt {
default_value = DEFAULT_CONFIG,
value_parser = Opt::parse_dir
)]
pub config: PathBuf,
/// Sets the URL for the configuration file.
#[arg(long, env = "GIT_CLIFF_CONFIG_URL", value_name = "URL", hide = !cfg!(feature = "remote"))]
pub config_url: Option<Url>,
/// Sets the working directory.
#[arg(
pub config: PathBuf,
/// Sets the URL for the configuration file.
#[arg(long, env = "GIT_CLIFF_CONFIG_URL", value_name = "URL", hide = !cfg!(feature = "remote"))]
pub config_url: Option<Url>,
/// Sets the working directory.
#[arg(
short,
long,
env = "GIT_CLIFF_WORKDIR",
value_name = "PATH",
value_parser = Opt::parse_dir
)]
pub workdir: Option<PathBuf>,
/// Sets the git repository.
#[arg(
pub workdir: Option<PathBuf>,
/// Sets the git repository.
#[arg(
short,
long,
env = "GIT_CLIFF_REPOSITORY",
@@ -113,67 +113,67 @@ pub struct Opt {
num_args(1..),
value_parser = Opt::parse_dir
)]
pub repository: Option<Vec<PathBuf>>,
/// Sets the path to include related commits.
#[arg(
pub repository: Option<Vec<PathBuf>>,
/// Sets the path to include related commits.
#[arg(
long,
env = "GIT_CLIFF_INCLUDE_PATH",
value_name = "PATTERN",
num_args(1..)
)]
pub include_path: Option<Vec<Pattern>>,
/// Sets the path to exclude related commits.
#[arg(
pub include_path: Option<Vec<Pattern>>,
/// Sets the path to exclude related commits.
#[arg(
long,
env = "GIT_CLIFF_EXCLUDE_PATH",
value_name = "PATTERN",
num_args(1..)
)]
pub exclude_path: Option<Vec<Pattern>>,
/// Sets the regex for matching git tags.
#[arg(long, env = "GIT_CLIFF_TAG_PATTERN", value_name = "PATTERN")]
pub tag_pattern: Option<Regex>,
/// Sets custom commit messages to include in the changelog.
#[arg(
pub exclude_path: Option<Vec<Pattern>>,
/// Sets the regex for matching git tags.
#[arg(long, env = "GIT_CLIFF_TAG_PATTERN", value_name = "PATTERN")]
pub tag_pattern: Option<Regex>,
/// Sets custom commit messages to include in the changelog.
#[arg(
long,
env = "GIT_CLIFF_WITH_COMMIT",
value_name = "MSG",
num_args(1..)
)]
pub with_commit: Option<Vec<String>>,
/// Sets custom message for the latest release.
#[arg(
pub with_commit: Option<Vec<String>>,
/// Sets custom message for the latest release.
#[arg(
long,
env = "GIT_CLIFF_WITH_TAG_MESSAGE",
value_name = "MSG",
num_args = 0..=1,
)]
pub with_tag_message: Option<String>,
/// Sets the tags to ignore in the changelog.
#[arg(long, env = "GIT_CLIFF_IGNORE_TAGS", value_name = "PATTERN")]
pub ignore_tags: Option<Regex>,
/// Sets the tags to count in the changelog.
#[arg(long, env = "GIT_CLIFF_COUNT_TAGS", value_name = "PATTERN")]
pub count_tags: Option<Regex>,
/// Sets commits that will be skipped in the changelog.
#[arg(
pub with_tag_message: Option<String>,
/// Sets the tags to ignore in the changelog.
#[arg(long, env = "GIT_CLIFF_IGNORE_TAGS", value_name = "PATTERN")]
pub ignore_tags: Option<Regex>,
/// Sets the tags to count in the changelog.
#[arg(long, env = "GIT_CLIFF_COUNT_TAGS", value_name = "PATTERN")]
pub count_tags: Option<Regex>,
/// Sets commits that will be skipped in the changelog.
#[arg(
long,
env = "GIT_CLIFF_SKIP_COMMIT",
value_name = "SHA1",
num_args(1..)
)]
pub skip_commit: Option<Vec<String>>,
/// Prepends entries to the given changelog file.
#[arg(
pub skip_commit: Option<Vec<String>>,
/// Prepends entries to the given changelog file.
#[arg(
short,
long,
env = "GIT_CLIFF_PREPEND",
value_name = "PATH",
value_parser = Opt::parse_dir
)]
pub prepend: Option<PathBuf>,
/// Writes output to the given file.
#[arg(
pub prepend: Option<PathBuf>,
/// Writes output to the given file.
#[arg(
short,
long,
env = "GIT_CLIFF_OUTPUT",
@@ -182,155 +182,155 @@ pub struct Opt {
num_args = 0..=1,
default_missing_value = DEFAULT_OUTPUT
)]
pub output: Option<PathBuf>,
/// Sets the tag for the latest version.
#[arg(
short,
long,
env = "GIT_CLIFF_TAG",
value_name = "TAG",
allow_hyphen_values = true
)]
pub tag: Option<String>,
/// Bumps the version for unreleased changes. Optionally with specified
/// version.
#[arg(
pub output: Option<PathBuf>,
/// Sets the tag for the latest version.
#[arg(
short,
long,
env = "GIT_CLIFF_TAG",
value_name = "TAG",
allow_hyphen_values = true
)]
pub tag: Option<String>,
/// Bumps the version for unreleased changes. Optionally with specified
/// version.
#[arg(
long,
value_name = "BUMP",
value_enum,
num_args = 0..=1,
default_missing_value = "auto",
value_parser = clap::value_parser!(BumpOption))]
pub bump: Option<BumpOption>,
/// Prints bumped version for unreleased changes.
#[arg(long, help_heading = Some("FLAGS"))]
pub bumped_version: bool,
/// Sets the template for the changelog body.
#[arg(
short,
long,
env = "GIT_CLIFF_TEMPLATE",
value_name = "TEMPLATE",
allow_hyphen_values = true
)]
pub body: Option<String>,
/// Processes the commits starting from the latest tag.
#[arg(short, long, help_heading = Some("FLAGS"))]
pub latest: bool,
/// Processes the commits that belong to the current tag.
#[arg(long, help_heading = Some("FLAGS"))]
pub current: bool,
/// Processes the commits that do not belong to a tag.
#[arg(short, long, help_heading = Some("FLAGS"))]
pub unreleased: bool,
/// Sorts the tags topologically.
#[arg(long, help_heading = Some("FLAGS"))]
pub topo_order: bool,
/// Include only the tags that belong to the current branch.
#[arg(long, help_heading = Some("FLAGS"))]
pub use_branch_tags: bool,
/// Disables the external command execution.
#[arg(long, help_heading = Some("FLAGS"))]
pub no_exec: bool,
/// Prints changelog context as JSON.
#[arg(short = 'x', long, help_heading = Some("FLAGS"))]
pub context: bool,
/// Generates changelog from a JSON context.
#[arg(
pub bump: Option<BumpOption>,
/// Prints bumped version for unreleased changes.
#[arg(long, help_heading = Some("FLAGS"))]
pub bumped_version: bool,
/// Sets the template for the changelog body.
#[arg(
short,
long,
env = "GIT_CLIFF_TEMPLATE",
value_name = "TEMPLATE",
allow_hyphen_values = true
)]
pub body: Option<String>,
/// Processes the commits starting from the latest tag.
#[arg(short, long, help_heading = Some("FLAGS"))]
pub latest: bool,
/// Processes the commits that belong to the current tag.
#[arg(long, help_heading = Some("FLAGS"))]
pub current: bool,
/// Processes the commits that do not belong to a tag.
#[arg(short, long, help_heading = Some("FLAGS"))]
pub unreleased: bool,
/// Sorts the tags topologically.
#[arg(long, help_heading = Some("FLAGS"))]
pub topo_order: bool,
/// Include only the tags that belong to the current branch.
#[arg(long, help_heading = Some("FLAGS"))]
pub use_branch_tags: bool,
/// Disables the external command execution.
#[arg(long, help_heading = Some("FLAGS"))]
pub no_exec: bool,
/// Prints changelog context as JSON.
#[arg(short = 'x', long, help_heading = Some("FLAGS"))]
pub context: bool,
/// Generates changelog from a JSON context.
#[arg(
long,
value_name = "PATH",
value_parser = Opt::parse_dir,
env = "GIT_CLIFF_CONTEXT",
)]
pub from_context: Option<PathBuf>,
/// Strips the given parts from the changelog.
#[arg(short, long, value_name = "PART", value_enum)]
pub strip: Option<Strip>,
/// Sets sorting of the commits inside sections.
#[arg(
pub from_context: Option<PathBuf>,
/// Strips the given parts from the changelog.
#[arg(short, long, value_name = "PART", value_enum)]
pub strip: Option<Strip>,
/// Sets sorting of the commits inside sections.
#[arg(
long,
value_enum,
default_value_t = Sort::Oldest
)]
pub sort: Sort,
/// Sets the commit range to process.
#[arg(value_name = "RANGE", help_heading = Some("ARGS"))]
pub range: Option<String>,
/// Sets the GitHub API token.
#[arg(
pub sort: Sort,
/// Sets the commit range to process.
#[arg(value_name = "RANGE", help_heading = Some("ARGS"))]
pub range: Option<String>,
/// Sets the GitHub API token.
#[arg(
long,
env = "GITHUB_TOKEN",
value_name = "TOKEN",
hide_env_values = true,
hide = !cfg!(feature = "github"),
)]
pub github_token: Option<SecretString>,
/// Sets the GitHub repository.
#[arg(
pub github_token: Option<SecretString>,
/// Sets the GitHub repository.
#[arg(
long,
env = "GITHUB_REPO",
value_parser = clap::value_parser!(RemoteValue),
value_name = "OWNER/REPO",
hide = !cfg!(feature = "github"),
)]
pub github_repo: Option<RemoteValue>,
/// Sets the GitLab API token.
#[arg(
pub github_repo: Option<RemoteValue>,
/// Sets the GitLab API token.
#[arg(
long,
env = "GITLAB_TOKEN",
value_name = "TOKEN",
hide_env_values = true,
hide = !cfg!(feature = "gitlab"),
)]
pub gitlab_token: Option<SecretString>,
/// Sets the GitLab repository.
#[arg(
pub gitlab_token: Option<SecretString>,
/// Sets the GitLab repository.
#[arg(
long,
env = "GITLAB_REPO",
value_parser = clap::value_parser!(RemoteValue),
value_name = "OWNER/REPO",
hide = !cfg!(feature = "gitlab"),
)]
pub gitlab_repo: Option<RemoteValue>,
/// Sets the Gitea API token.
#[arg(
pub gitlab_repo: Option<RemoteValue>,
/// Sets the Gitea API token.
#[arg(
long,
env = "GITEA_TOKEN",
value_name = "TOKEN",
hide_env_values = true,
hide = !cfg!(feature = "gitea"),
)]
pub gitea_token: Option<SecretString>,
/// Sets the Gitea repository.
#[arg(
pub gitea_token: Option<SecretString>,
/// Sets the Gitea repository.
#[arg(
long,
env = "GITEA_REPO",
value_parser = clap::value_parser!(RemoteValue),
value_name = "OWNER/REPO",
hide = !cfg!(feature = "gitea"),
)]
pub gitea_repo: Option<RemoteValue>,
/// Sets the Bitbucket API token.
#[arg(
pub gitea_repo: Option<RemoteValue>,
/// Sets the Bitbucket API token.
#[arg(
long,
env = "BITBUCKET_TOKEN",
value_name = "TOKEN",
hide_env_values = true,
hide = !cfg!(feature = "bitbucket"),
)]
pub bitbucket_token: Option<SecretString>,
/// Sets the Bitbucket repository.
#[arg(
pub bitbucket_token: Option<SecretString>,
/// Sets the Bitbucket repository.
#[arg(
long,
env = "BITBUCKET_REPO",
value_parser = clap::value_parser!(RemoteValue),
value_name = "OWNER/REPO",
hide = !cfg!(feature = "bitbucket"),
)]
pub bitbucket_repo: Option<RemoteValue>,
/// Load TLS certificates from the native certificate store.
#[arg(long, help_heading = Some("FLAGS"), hide = !cfg!(feature = "remote"))]
pub use_native_tls: bool,
pub bitbucket_repo: Option<RemoteValue>,
/// Load TLS certificates from the native certificate store.
#[arg(long, help_heading = Some("FLAGS"), hide = !cfg!(feature = "remote"))]
pub use_native_tls: bool,
}
/// Custom type for the remote value.
@@ -338,10 +338,10 @@ pub struct Opt {
pub struct RemoteValue(pub Remote);
impl ValueParserFactory for RemoteValue {
type Parser = RemoteValueParser;
fn value_parser() -> Self::Parser {
RemoteValueParser
}
type Parser = RemoteValueParser;
fn value_parser() -> Self::Parser {
RemoteValueParser
}
}
/// Parser for the remote value.
@@ -349,49 +349,49 @@ impl ValueParserFactory for RemoteValue {
pub struct RemoteValueParser;
impl TypedValueParser for RemoteValueParser {
type Value = RemoteValue;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
let inner = clap::builder::StringValueParser::new();
let mut value = inner.parse_ref(cmd, arg, value)?;
if let Ok(url) = Url::parse(&value) {
value = url.path().trim_start_matches('/').to_string();
}
let parts = value.rsplit_once('/');
if let Some((owner, repo)) = parts {
Ok(RemoteValue(Remote::new(
owner.to_string(),
repo.to_string(),
)))
} else {
let mut err = clap::Error::new(ErrorKind::ValueValidation).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(
ContextKind::InvalidArg,
ContextValue::String(arg.to_string()),
);
}
err.insert(ContextKind::InvalidValue, ContextValue::String(value));
Err(err)
}
}
type Value = RemoteValue;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
let inner = clap::builder::StringValueParser::new();
let mut value = inner.parse_ref(cmd, arg, value)?;
if let Ok(url) = Url::parse(&value) {
value = url.path().trim_start_matches('/').to_string();
}
let parts = value.rsplit_once('/');
if let Some((owner, repo)) = parts {
Ok(RemoteValue(Remote::new(
owner.to_string(),
repo.to_string(),
)))
} else {
let mut err = clap::Error::new(ErrorKind::ValueValidation).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(
ContextKind::InvalidArg,
ContextValue::String(arg.to_string()),
);
}
err.insert(ContextKind::InvalidValue, ContextValue::String(value));
Err(err)
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum BumpOption {
Auto,
Specific(BumpType),
Auto,
Specific(BumpType),
}
impl ValueParserFactory for BumpOption {
type Parser = BumpOptionParser;
fn value_parser() -> Self::Parser {
BumpOptionParser
}
type Parser = BumpOptionParser;
fn value_parser() -> Self::Parser {
BumpOptionParser
}
}
/// Parser for bump type.
@@ -399,140 +399,132 @@ impl ValueParserFactory for BumpOption {
pub struct BumpOptionParser;
impl TypedValueParser for BumpOptionParser {
type Value = BumpOption;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
let inner = clap::builder::StringValueParser::new();
let value = inner.parse_ref(cmd, arg, value)?;
match value.as_str() {
"auto" => Ok(BumpOption::Auto),
"major" => Ok(BumpOption::Specific(BumpType::Major)),
"minor" => Ok(BumpOption::Specific(BumpType::Minor)),
"patch" => Ok(BumpOption::Specific(BumpType::Patch)),
_ => {
let mut err =
clap::Error::new(ErrorKind::ValueValidation).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(
ContextKind::InvalidArg,
ContextValue::String(arg.to_string()),
);
}
err.insert(ContextKind::InvalidValue, ContextValue::String(value));
Err(err)
}
}
}
type Value = BumpOption;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
let inner = clap::builder::StringValueParser::new();
let value = inner.parse_ref(cmd, arg, value)?;
match value.as_str() {
"auto" => Ok(BumpOption::Auto),
"major" => Ok(BumpOption::Specific(BumpType::Major)),
"minor" => Ok(BumpOption::Specific(BumpType::Minor)),
"patch" => Ok(BumpOption::Specific(BumpType::Patch)),
_ => {
let mut err = clap::Error::new(ErrorKind::ValueValidation).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(
ContextKind::InvalidArg,
ContextValue::String(arg.to_string()),
);
}
err.insert(ContextKind::InvalidValue, ContextValue::String(value));
Err(err)
}
}
}
}
impl Opt {
/// Custom string parser for directories.
///
/// Expands the tilde (`~`) character in the beginning of the
/// input string into contents of the path returned by [`home_dir`].
///
/// [`home_dir`]: dirs::home_dir
fn parse_dir(dir: &str) -> Result<PathBuf, String> {
Ok(PathBuf::from(shellexpand::tilde(dir).to_string()))
}
/// Custom string parser for directories.
///
/// Expands the tilde (`~`) character in the beginning of the
/// input string into contents of the path returned by [`home_dir`].
///
/// [`home_dir`]: dirs::home_dir
fn parse_dir(dir: &str) -> Result<PathBuf, String> {
Ok(PathBuf::from(shellexpand::tilde(dir).to_string()))
}
}
#[cfg(test)]
mod tests {
use std::ffi::OsStr;
use std::ffi::OsStr;
use clap::CommandFactory;
use clap::CommandFactory;
use super::*;
use super::*;
#[test]
fn verify_cli() {
Opt::command().debug_assert();
}
#[test]
fn verify_cli() {
Opt::command().debug_assert();
}
#[test]
fn path_tilde_expansion() {
let home_dir = dirs::home_dir().expect("cannot retrieve home directory");
let dir = Opt::parse_dir("~/").expect("cannot expand tilde");
assert_eq!(home_dir, dir);
}
#[test]
fn path_tilde_expansion() {
let home_dir = dirs::home_dir().expect("cannot retrieve home directory");
let dir = Opt::parse_dir("~/").expect("cannot expand tilde");
assert_eq!(home_dir, dir);
}
#[test]
fn remote_value_parser() -> Result<(), clap::Error> {
let remote_value_parser = RemoteValueParser;
assert_eq!(
RemoteValue(Remote::new("test", "repo")),
remote_value_parser.parse_ref(
&Opt::command(),
None,
OsStr::new("test/repo")
)?
);
assert_eq!(
RemoteValue(Remote::new("gitlab/group/test", "repo")),
remote_value_parser.parse_ref(
&Opt::command(),
None,
OsStr::new("gitlab/group/test/repo")
)?
);
assert_eq!(
RemoteValue(Remote::new("test", "testrepo")),
remote_value_parser.parse_ref(
&Opt::command(),
None,
OsStr::new("https://github.com/test/testrepo")
)?
);
assert_eq!(
RemoteValue(Remote::new("archlinux/packaging/packages", "arch-repro-status")),
remote_value_parser.parse_ref(
&Opt::command(),
None,
OsStr::new("https://gitlab.archlinux.org/archlinux/packaging/packages/arch-repro-status")
)?
);
assert!(
remote_value_parser
.parse_ref(&Opt::command(), None, OsStr::new("test"))
.is_err()
);
assert!(
remote_value_parser
.parse_ref(&Opt::command(), None, OsStr::new(""))
.is_err()
);
Ok(())
}
#[test]
fn remote_value_parser() -> Result<(), clap::Error> {
let remote_value_parser = RemoteValueParser;
assert_eq!(
RemoteValue(Remote::new("test", "repo")),
remote_value_parser.parse_ref(&Opt::command(), None, OsStr::new("test/repo"))?
);
assert_eq!(
RemoteValue(Remote::new("gitlab/group/test", "repo")),
remote_value_parser.parse_ref(
&Opt::command(),
None,
OsStr::new("gitlab/group/test/repo")
)?
);
assert_eq!(
RemoteValue(Remote::new("test", "testrepo")),
remote_value_parser.parse_ref(
&Opt::command(),
None,
OsStr::new("https://github.com/test/testrepo")
)?
);
assert_eq!(
RemoteValue(Remote::new(
"archlinux/packaging/packages",
"arch-repro-status"
)),
remote_value_parser.parse_ref(
&Opt::command(),
None,
OsStr::new(
"https://gitlab.archlinux.org/archlinux/packaging/packages/arch-repro-status"
)
)?
);
assert!(
remote_value_parser
.parse_ref(&Opt::command(), None, OsStr::new("test"))
.is_err()
);
assert!(
remote_value_parser
.parse_ref(&Opt::command(), None, OsStr::new(""))
.is_err()
);
Ok(())
}
#[test]
fn bump_option_parser() -> Result<(), clap::Error> {
let bump_option_parser = BumpOptionParser;
assert_eq!(
BumpOption::Auto,
bump_option_parser.parse_ref(
&Opt::command(),
None,
OsStr::new("auto")
)?
);
assert!(
bump_option_parser
.parse_ref(&Opt::command(), None, OsStr::new("test"))
.is_err()
);
assert_eq!(
BumpOption::Specific(BumpType::Major),
bump_option_parser.parse_ref(
&Opt::command(),
None,
OsStr::new("major")
)?
);
Ok(())
}
#[test]
fn bump_option_parser() -> Result<(), clap::Error> {
let bump_option_parser = BumpOptionParser;
assert_eq!(
BumpOption::Auto,
bump_option_parser.parse_ref(&Opt::command(), None, OsStr::new("auto"))?
);
assert!(
bump_option_parser
.parse_ref(&Opt::command(), None, OsStr::new("test"))
.is_err()
);
assert_eq!(
BumpOption::Specific(BumpType::Major),
bump_option_parser.parse_ref(&Opt::command(), None, OsStr::new("major"))?
);
Ok(())
}
}
+7 -12
View File
@@ -10,16 +10,11 @@ use git_cliff::args::Opt;
/// in a directory specified by the environment variable `OUT_DIR`.
/// See <https://doc.rust-lang.org/cargo/reference/environment-variables.html>
fn main() -> Result<()> {
let out_dir = env::var("OUT_DIR").expect("OUT_DIR is not set");
let mut app = Opt::command();
for &shell in Shell::value_variants() {
clap_complete::generate_to(
shell,
&mut app,
env!("CARGO_PKG_NAME"),
&out_dir,
)?;
}
println!("Completion scripts are generated in {out_dir:?}");
Ok(())
let out_dir = env::var("OUT_DIR").expect("OUT_DIR is not set");
let mut app = Opt::command();
for &shell in Shell::value_variants() {
clap_complete::generate_to(shell, &mut app, env!("CARGO_PKG_NAME"), &out_dir)?;
}
println!("Completion scripts are generated in {out_dir:?}");
Ok(())
}
+9 -10
View File
@@ -11,14 +11,13 @@ use git_cliff::args::Opt;
/// in a directory specified by the environment variable `OUT_DIR`.
/// See <https://doc.rust-lang.org/cargo/reference/environment-variables.html>
fn main() -> Result<()> {
let out_dir = env::var("OUT_DIR").expect("OUT_DIR is not set");
let out_path =
PathBuf::from(out_dir).join(format!("{}.1", env!("CARGO_PKG_NAME")));
let app = Opt::command();
let man = Man::new(app);
let mut buffer = Vec::<u8>::new();
man.render(&mut buffer)?;
fs::write(&out_path, buffer)?;
println!("Man page is generated at {out_path:?}");
Ok(())
let out_dir = env::var("OUT_DIR").expect("OUT_DIR is not set");
let out_path = PathBuf::from(out_dir).join(format!("{}.1", env!("CARGO_PKG_NAME")));
let app = Opt::command();
let man = Man::new(app);
let mut buffer = Vec::<u8>::new();
man.render(&mut buffer)?;
fs::write(&out_path, buffer)?;
println!("Man page is generated at {out_path:?}");
Ok(())
}
+669 -698
View File
File diff suppressed because it is too large Load Diff
+105 -123
View File
@@ -17,58 +17,58 @@ static MAX_MODULE_WIDTH: AtomicUsize = AtomicUsize::new(0);
/// Wrapper for the padded values.
struct Padded<T> {
value: T,
width: usize,
value: T,
width: usize,
}
impl<T: fmt::Display> fmt::Display for Padded<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{: <width$}", self.value, width = self.width)
}
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{: <width$}", self.value, width = self.width)
}
}
/// Returns the max width of the target.
fn max_target_width(target: &str) -> usize {
let max_width = MAX_MODULE_WIDTH.load(Ordering::Relaxed);
if max_width < target.len() {
MAX_MODULE_WIDTH.store(target.len(), Ordering::Relaxed);
target.len()
} else {
max_width
}
let max_width = MAX_MODULE_WIDTH.load(Ordering::Relaxed);
if max_width < target.len() {
MAX_MODULE_WIDTH.store(target.len(), Ordering::Relaxed);
target.len()
} else {
max_width
}
}
/// Adds colors to the given level and returns it.
fn colored_level(style: &mut Style, level: Level) -> StyledValue<'_, &'static str> {
match level {
Level::Trace => style.set_color(Color::Magenta).value("TRACE"),
Level::Debug => style.set_color(Color::Blue).value("DEBUG"),
Level::Info => style.set_color(Color::Green).value("INFO "),
Level::Warn => style.set_color(Color::Yellow).value("WARN "),
Level::Error => style.set_color(Color::Red).value("ERROR"),
}
match level {
Level::Trace => style.set_color(Color::Magenta).value("TRACE"),
Level::Debug => style.set_color(Color::Blue).value("DEBUG"),
Level::Info => style.set_color(Color::Green).value("INFO "),
Level::Warn => style.set_color(Color::Yellow).value("WARN "),
Level::Error => style.set_color(Color::Red).value("ERROR"),
}
}
#[cfg(feature = "remote")]
lazy_static::lazy_static! {
/// Lazily initialized progress bar.
pub static ref PROGRESS_BAR: ProgressBar = {
let progress_bar = ProgressBar::new_spinner();
progress_bar.set_style(
ProgressStyle::with_template("{spinner:.green} {msg}")
.unwrap()
.tick_strings(&[
"▹▹▹▹▹",
"▸▹▹▹▹",
"▹▸▹▹▹",
"▹▹▸▹▹",
"▹▹▹▸▹",
"▹▹▹▹▸",
"▪▪▪▪▪",
]),
);
progress_bar
};
/// Lazily initialized progress bar.
pub static ref PROGRESS_BAR: ProgressBar = {
let progress_bar = ProgressBar::new_spinner();
progress_bar.set_style(
ProgressStyle::with_template("{spinner:.green} {msg}")
.unwrap()
.tick_strings(&[
"▹▹▹▹▹",
"▸▹▹▹▹",
"▹▸▹▹▹",
"▹▹▸▹▹",
"▹▹▹▸▹",
"▹▹▹▹▸",
"▪▪▪▪▪",
]),
);
progress_bar
};
}
/// Initializes the global logger.
@@ -77,99 +77,81 @@ lazy_static::lazy_static! {
/// by the network operations that are related to GitHub.
#[allow(unreachable_code, clippy::needless_return)]
pub fn init() -> Result<()> {
let mut builder = Builder::new();
builder.format(move |f, record| {
let target = record.target();
let max_width = max_target_width(target);
let mut builder = Builder::new();
builder.format(move |f, record| {
let target = record.target();
let max_width = max_target_width(target);
let mut style = f.style();
let level = colored_level(&mut style, record.level());
let mut style = f.style();
let level = colored_level(&mut style, record.level());
let mut style = f.style();
let target = style.set_bold(true).value(Padded {
value: target,
width: max_width,
});
let mut style = f.style();
let target = style.set_bold(true).value(Padded {
value: target,
width: max_width,
});
#[cfg(feature = "github")]
{
let message = record.args().to_string();
if message
.starts_with(git_cliff_core::remote::github::START_FETCHING_MSG)
{
PROGRESS_BAR
.enable_steady_tick(std::time::Duration::from_millis(80));
PROGRESS_BAR.set_message(message);
return Ok(());
} else if message
.starts_with(git_cliff_core::remote::github::FINISHED_FETCHING_MSG)
{
PROGRESS_BAR.finish_and_clear();
return Ok(());
}
}
#[cfg(feature = "github")]
{
let message = record.args().to_string();
if message.starts_with(git_cliff_core::remote::github::START_FETCHING_MSG) {
PROGRESS_BAR.enable_steady_tick(std::time::Duration::from_millis(80));
PROGRESS_BAR.set_message(message);
return Ok(());
} else if message.starts_with(git_cliff_core::remote::github::FINISHED_FETCHING_MSG) {
PROGRESS_BAR.finish_and_clear();
return Ok(());
}
}
#[cfg(feature = "gitlab")]
{
let message = record.args().to_string();
if message
.starts_with(git_cliff_core::remote::gitlab::START_FETCHING_MSG)
{
PROGRESS_BAR
.enable_steady_tick(std::time::Duration::from_millis(80));
PROGRESS_BAR.set_message(message);
return Ok(());
} else if message
.starts_with(git_cliff_core::remote::gitlab::FINISHED_FETCHING_MSG)
{
PROGRESS_BAR.finish_and_clear();
return Ok(());
}
}
#[cfg(feature = "gitlab")]
{
let message = record.args().to_string();
if message.starts_with(git_cliff_core::remote::gitlab::START_FETCHING_MSG) {
PROGRESS_BAR.enable_steady_tick(std::time::Duration::from_millis(80));
PROGRESS_BAR.set_message(message);
return Ok(());
} else if message.starts_with(git_cliff_core::remote::gitlab::FINISHED_FETCHING_MSG) {
PROGRESS_BAR.finish_and_clear();
return Ok(());
}
}
#[cfg(feature = "gitea")]
{
let message = record.args().to_string();
if message.starts_with(git_cliff_core::remote::gitea::START_FETCHING_MSG)
{
PROGRESS_BAR
.enable_steady_tick(std::time::Duration::from_millis(80));
PROGRESS_BAR.set_message(message);
return Ok(());
} else if message
.starts_with(git_cliff_core::remote::gitea::FINISHED_FETCHING_MSG)
{
PROGRESS_BAR.finish_and_clear();
return Ok(());
}
}
#[cfg(feature = "gitea")]
{
let message = record.args().to_string();
if message.starts_with(git_cliff_core::remote::gitea::START_FETCHING_MSG) {
PROGRESS_BAR.enable_steady_tick(std::time::Duration::from_millis(80));
PROGRESS_BAR.set_message(message);
return Ok(());
} else if message.starts_with(git_cliff_core::remote::gitea::FINISHED_FETCHING_MSG) {
PROGRESS_BAR.finish_and_clear();
return Ok(());
}
}
#[cfg(feature = "bitbucket")]
{
let message = record.args().to_string();
if message
.starts_with(git_cliff_core::remote::bitbucket::START_FETCHING_MSG)
{
PROGRESS_BAR
.enable_steady_tick(std::time::Duration::from_millis(80));
PROGRESS_BAR.set_message(message);
return Ok(());
} else if message.starts_with(
git_cliff_core::remote::bitbucket::FINISHED_FETCHING_MSG,
) {
PROGRESS_BAR.finish_and_clear();
return Ok(());
}
}
#[cfg(feature = "bitbucket")]
{
let message = record.args().to_string();
if message.starts_with(git_cliff_core::remote::bitbucket::START_FETCHING_MSG) {
PROGRESS_BAR.enable_steady_tick(std::time::Duration::from_millis(80));
PROGRESS_BAR.set_message(message);
return Ok(());
} else if message.starts_with(git_cliff_core::remote::bitbucket::FINISHED_FETCHING_MSG)
{
PROGRESS_BAR.finish_and_clear();
return Ok(());
}
}
writeln!(f, " {} {} > {}", level, target, record.args())
});
writeln!(f, " {} {} > {}", level, target, record.args())
});
if let Ok(var) = env::var(LOGGER_ENV) {
builder.parse_filters(&var);
}
if let Ok(var) = env::var(LOGGER_ENV) {
builder.parse_filters(&var);
}
builder
.try_init()
.map_err(|e| Error::LoggerError(e.to_string()))
builder
.try_init()
.map_err(|e| Error::LoggerError(e.to_string()))
}
+34 -34
View File
@@ -10,42 +10,42 @@ use git_cliff_core::error::Result;
mod profiler;
fn main() -> Result<()> {
// Parse the command line arguments
let args = Opt::parse();
if args.verbose == 1 {
unsafe { env::set_var("RUST_LOG", "debug") };
} else if args.verbose > 1 {
unsafe { env::set_var("RUST_LOG", "trace") };
} else if env::var_os("RUST_LOG").is_none() {
unsafe { env::set_var("RUST_LOG", "info") };
}
logger::init()?;
// Parse the command line arguments
let args = Opt::parse();
if args.verbose == 1 {
unsafe { env::set_var("RUST_LOG", "debug") };
} else if args.verbose > 1 {
unsafe { env::set_var("RUST_LOG", "trace") };
} else if env::var_os("RUST_LOG").is_none() {
unsafe { env::set_var("RUST_LOG", "info") };
}
logger::init()?;
// Initialize the profiler guard if the feature is enabled
let mut _profiler_guard = None;
#[cfg(feature = "profiler")]
{
_profiler_guard = profiler::start_profiling();
}
#[cfg(not(feature = "profiler"))]
{
_profiler_guard = Some(());
}
// Initialize the profiler guard if the feature is enabled
let mut _profiler_guard = None;
#[cfg(feature = "profiler")]
{
_profiler_guard = profiler::start_profiling();
}
#[cfg(not(feature = "profiler"))]
{
_profiler_guard = Some(());
}
// Run git-cliff
let exit_code = match git_cliff::run(args) {
Ok(()) => 0,
Err(e) => {
log::error!("{}", e);
1
}
};
// Run git-cliff
let exit_code = match git_cliff::run(args) {
Ok(()) => 0,
Err(e) => {
log::error!("{}", e);
1
}
};
// Report the profiler if the feature is enabled
#[cfg(feature = "profiler")]
{
profiler::finish_profiling(_profiler_guard)?;
}
// Report the profiler if the feature is enabled
#[cfg(feature = "profiler")]
{
profiler::finish_profiling(_profiler_guard)?;
}
process::exit(exit_code);
process::exit(exit_code);
}
+40 -42
View File
@@ -2,51 +2,49 @@ use git_cliff_core::error::Result;
/// Creates a profiler guard and returns it.
pub(crate) fn start_profiling() -> Option<pprof::ProfilerGuard<'static>> {
match pprof::ProfilerGuardBuilder::default()
.frequency(1000)
.blocklist(&["libc", "libgcc", "pthread", "vdso"])
.build()
{
Ok(guard) => Some(guard),
Err(e) => {
log::error!("failed to build profiler guard: {e}");
None
}
}
match pprof::ProfilerGuardBuilder::default()
.frequency(1000)
.blocklist(&["libc", "libgcc", "pthread", "vdso"])
.build()
{
Ok(guard) => Some(guard),
Err(e) => {
log::error!("failed to build profiler guard: {e}");
None
}
}
}
/// Reports the profiling results.
pub(crate) fn finish_profiling(
profiler_guard: Option<pprof::ProfilerGuard>,
) -> Result<()> {
match profiler_guard
.expect("failed to retrieve profiler guard")
.report()
.build()
{
Ok(report) => {
#[cfg(feature = "profiler-flamegraph")]
{
use std::fs::File;
let random = rand::random::<u64>();
let file = File::create(format!(
"{}.{random}.flamegraph.svg",
env!("CARGO_PKG_NAME"),
))?;
if let Err(e) = report.flamegraph(file) {
log::error!("failed to create flamegraph file: {e}");
}
}
pub(crate) fn finish_profiling(profiler_guard: Option<pprof::ProfilerGuard>) -> Result<()> {
match profiler_guard
.expect("failed to retrieve profiler guard")
.report()
.build()
{
Ok(report) => {
#[cfg(feature = "profiler-flamegraph")]
{
use std::fs::File;
let random = rand::random::<u64>();
let file = File::create(format!(
"{}.{random}.flamegraph.svg",
env!("CARGO_PKG_NAME"),
))?;
if let Err(e) = report.flamegraph(file) {
log::error!("failed to create flamegraph file: {e}");
}
}
#[cfg(not(feature = "profiler-flamegraph"))]
{
log::info!("profiling report: {:?}", &report);
}
}
Err(e) => {
log::error!("failed to build profiler report: {e}");
}
}
#[cfg(not(feature = "profiler-flamegraph"))]
{
log::info!("profiling report: {:?}", &report);
}
}
Err(e) => {
log::error!("failed to build profiler report: {e}");
}
}
Ok(())
Ok(())
}
+2
View File
@@ -0,0 +1,2 @@
[toolchain]
channel = "stable"
+4 -6
View File
@@ -1,18 +1,16 @@
edition = "2024"
max_width = 85
max_width = 100
newline_style = "Unix"
hard_tabs = true
tab_spaces = 4
use_field_init_shorthand = true
reorder_imports = true
use_field_init_shorthand = true
# Unstable options
# unstable options that require rust nightly
binop_separator = "Back"
comment_width = 100
format_code_in_doc_comments = true
format_strings = true
group_imports = "StdExternalCrate"
imports_granularity = "Module"
normalize_doc_attributes = true
overflow_delimited_expr = true
struct_field_align_threshold = 20
wrap_comments = true