diff --git a/Cargo.lock b/Cargo.lock index b5d675478a..4bce764b08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -732,19 +732,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "env_logger" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" -dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", -] - [[package]] name = "equator" version = "0.4.2" @@ -1003,12 +990,12 @@ dependencies = [ "clap_complete", "clap_mangen", "dirs", - "env_logger", "git-cliff-core", "glob", "indicatif", "lazy_static", "log", + "owo-colors", "pathdiff", "pprof", "pretty_assertions", @@ -1017,6 +1004,9 @@ dependencies = [ "reqwest", "secrecy", "shellexpand", + "tracing", + "tracing-indicatif", + "tracing-subscriber", "update-informer", "url", ] @@ -1060,6 +1050,7 @@ dependencies = [ "time", "tokio", "toml", + "tracing", "url", "urlencoding", ] @@ -1284,12 +1275,6 @@ dependencies = [ "libm", ] -[[package]] -name = "humantime" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" - [[package]] name = "hyper" version = "1.7.0" @@ -1552,6 +1537,7 @@ dependencies = [ "portable-atomic", "unicode-width 0.2.2", "unit-prefix", + "vt100", "web-time", ] @@ -1771,6 +1757,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.7.6" @@ -1860,6 +1855,15 @@ dependencies = [ "libc", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1918,6 +1922,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "owo-colors" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" + [[package]] name = "parse-zoneinfo" version = "0.3.1" @@ -2713,6 +2723,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shellexpand" version = "3.1.1" @@ -2921,15 +2940,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "terminal_size" version = "0.4.3" @@ -2980,6 +2990,15 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.44" @@ -3187,21 +3206,89 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-indicatif" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1ef6990e0438749f0080573248e96631171a0b5ddfddde119aa5ba8c3a9c47e" +dependencies = [ + "indicatif", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ + "log", "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", ] [[package]] @@ -3363,6 +3450,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" @@ -3381,6 +3474,27 @@ version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" +[[package]] +name = "vt100" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ff75fb8fa83e609e685106df4faeffdf3a735d3c74ebce97ec557d5d36fd9" +dependencies = [ + "itoa", + "unicode-width 0.2.2", + "vte", +] + +[[package]] +name = "vte" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd" +dependencies = [ + "arrayvec", + "memchr", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 237458d9df..05df44621b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ reqwest = { version = "0.12.25", default-features = false, features = [ "json", "zstd", ] } +tracing = "0.1.44" [profile.dev] opt-level = 0 diff --git a/git-cliff-core/Cargo.toml b/git-cliff-core/Cargo.toml index 81b218265b..0c0c46baa8 100644 --- a/git-cliff-core/Cargo.toml +++ b/git-cliff-core/Cargo.toml @@ -46,6 +46,10 @@ gitea = ["remote"] ## You can turn this off if you don't use Azure DevOps and don't want ## to make network requests to the Azure DevOps API. azure_devops = ["remote"] +## Enable tracing instrumentation in git-cliff-core. +## This is intentionally feature-gated to keep the core library +## lightweight and to allow downstream users to opt in to tracing. +tracing = ["dep:tracing"] [dependencies] glob.workspace = true @@ -83,6 +87,7 @@ urlencoding = "2.1.3" cacache = { version = "13.1.0", features = ["mmap", "tokio-runtime"], default-features = false } time = "0.3.44" chrono = { version = "0.4.41", features = ["serde"] } +tracing = { workspace = true, optional = true } [dependencies.git2] version = "0.20.3" diff --git a/git-cliff-core/src/changelog.rs b/git-cliff-core/src/changelog.rs index 0094706016..e8976454ba 100644 --- a/git-cliff-core/src/changelog.rs +++ b/git-cliff-core/src/changelog.rs @@ -83,6 +83,13 @@ impl<'a> Changelog<'a> { } /// Processes a single commit and returns/logs the result. + #[cfg_attr( + feature = "tracing", + tracing::instrument( + skip_all, + fields(id = commit.id) + ) + )] fn process_commit(commit: &Commit<'a>, git_config: &GitConfig) -> Option> { match commit.process(git_config) { Ok(commit) => Some(commit), @@ -104,6 +111,13 @@ impl<'a> Changelog<'a> { /// Checks the commits and returns an error if any unconventional commits /// are found. + #[cfg_attr( + feature = "tracing", + tracing::instrument( + skip_all, + fields(commits = commits.len()) + ) + )] fn check_conventional_commits(commits: &Vec>) -> Result<()> { log::debug!("Verifying that all commits are conventional"); let mut unconventional_count = 0; @@ -132,6 +146,13 @@ impl<'a> Changelog<'a> { /// Checks the commits and returns an error if any commits are not matched /// by any commit parser. + #[cfg_attr( + feature = "tracing", + tracing::instrument( + skip_all, + fields(commits = commits.len()) + ) + )] fn check_unmatched_commits(commits: &Vec>) -> Result<()> { log::debug!("Verifying that no commits are unmatched by commit parsers"); let mut unmatched_count = 0; @@ -159,6 +180,14 @@ impl<'a> Changelog<'a> { Ok(()) } + /// Processes a commit list by applying parsing, splitting, and validation rules. + #[cfg_attr( + feature = "tracing", + tracing::instrument( + skip_all, + fields(commits = commits.len()) + ) + )] fn process_commit_list(commits: &mut Vec>, git_config: &GitConfig) -> Result<()> { *commits = commits .iter() @@ -198,6 +227,13 @@ impl<'a> Changelog<'a> { /// Processes the commits and omits the ones that doesn't match the /// criteria set by configuration file. + #[cfg_attr( + feature = "tracing", + tracing::instrument( + skip_all, + fields(releases = self.releases.len()) + ) + )] fn process_commits(&mut self) -> Result<()> { log::debug!("Processing the commits"); for release in self.releases.iter_mut() { @@ -210,6 +246,13 @@ impl<'a> Changelog<'a> { } /// Processes the releases and filters them out based on the configuration. + #[cfg_attr( + feature = "tracing", + tracing::instrument( + skip_all, + fields(releases = self.releases.len()) + ) + )] fn process_releases(&mut self) { log::debug!("Processing {} release(s)", self.releases.len()); let skip_regex = self.config.git.skip_tags.as_ref(); @@ -275,6 +318,7 @@ impl<'a> Changelog<'a> { /// If no GitHub related variable is used in the template then this function /// returns empty vectors. #[cfg(feature = "github")] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] fn get_github_metadata(&self, ref_name: Option<&str>) -> Result { use crate::remote::github; if self.config.remote.github.is_custom || @@ -286,12 +330,7 @@ impl<'a> Changelog<'a> { .unwrap_or(false) { let github_client = GitHubClient::try_from(self.config.remote.github.clone())?; - log::info!( - "{} ({})", - github::START_FETCHING_MSG, - self.config.remote.github - ); - let data = tokio::runtime::Builder::new_multi_thread() + tokio::runtime::Builder::new_multi_thread() .enable_all() .build()? .block_on(async { @@ -302,9 +341,7 @@ impl<'a> Changelog<'a> { log::debug!("Number of GitHub commits: {}", commits.len()); log::debug!("Number of GitHub pull requests: {}", pull_requests.len()); Ok((commits, pull_requests)) - }); - log::info!("{}", github::FINISHED_FETCHING_MSG); - data + }) } else { Ok((vec![], vec![])) } @@ -326,6 +363,7 @@ impl<'a> Changelog<'a> { /// If no GitLab related variable is used in the template then this function /// returns empty vectors. #[cfg(feature = "gitlab")] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] fn get_gitlab_metadata(&self, ref_name: Option<&str>) -> Result { use crate::remote::gitlab; if self.config.remote.gitlab.is_custom || @@ -337,12 +375,7 @@ impl<'a> Changelog<'a> { .unwrap_or(false) { let gitlab_client = GitLabClient::try_from(self.config.remote.gitlab.clone())?; - log::info!( - "{} ({})", - gitlab::START_FETCHING_MSG, - self.config.remote.gitlab - ); - let data = tokio::runtime::Builder::new_multi_thread() + tokio::runtime::Builder::new_multi_thread() .enable_all() .build()? .block_on(async { @@ -362,9 +395,7 @@ impl<'a> Changelog<'a> { log::debug!("Number of GitLab commits: {}", commits.len()); log::debug!("Number of GitLab merge requests: {}", merge_requests.len()); Ok((commits, merge_requests)) - }); - log::info!("{}", gitlab::FINISHED_FETCHING_MSG); - data + }) } else { Ok((vec![], vec![])) } @@ -384,6 +415,7 @@ impl<'a> Changelog<'a> { /// If no Gitea related variable is used in the template then this function /// returns empty vectors. #[cfg(feature = "gitea")] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] fn get_gitea_metadata(&self, ref_name: Option<&str>) -> Result { use crate::remote::gitea; if self.config.remote.gitea.is_custom || @@ -395,12 +427,7 @@ impl<'a> Changelog<'a> { .unwrap_or(false) { let gitea_client = GiteaClient::try_from(self.config.remote.gitea.clone())?; - log::info!( - "{} ({})", - gitea::START_FETCHING_MSG, - self.config.remote.gitea - ); - let data = tokio::runtime::Builder::new_multi_thread() + tokio::runtime::Builder::new_multi_thread() .enable_all() .build()? .block_on(async { @@ -411,9 +438,7 @@ impl<'a> Changelog<'a> { log::debug!("Number of Gitea commits: {}", commits.len()); log::debug!("Number of Gitea pull requests: {}", pull_requests.len()); Ok((commits, pull_requests)) - }); - log::info!("{}", gitea::FINISHED_FETCHING_MSG); - data + }) } else { Ok((vec![], vec![])) } @@ -435,6 +460,7 @@ impl<'a> Changelog<'a> { /// If no bitbucket related variable is used in the template then this /// function returns empty vectors. #[cfg(feature = "bitbucket")] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] fn get_bitbucket_metadata( &self, ref_name: Option<&str>, @@ -449,12 +475,7 @@ impl<'a> Changelog<'a> { .unwrap_or(false) { let bitbucket_client = BitbucketClient::try_from(self.config.remote.bitbucket.clone())?; - log::info!( - "{} ({})", - bitbucket::START_FETCHING_MSG, - self.config.remote.bitbucket - ); - let data = tokio::runtime::Builder::new_multi_thread() + tokio::runtime::Builder::new_multi_thread() .enable_all() .build()? .block_on(async { @@ -465,9 +486,7 @@ impl<'a> Changelog<'a> { log::debug!("Number of Bitbucket commits: {}", commits.len()); log::debug!("Number of Bitbucket pull requests: {}", pull_requests.len()); Ok((commits, pull_requests)) - }); - log::info!("{}", bitbucket::FINISHED_FETCHING_MSG); - data + }) } else { Ok((vec![], vec![])) } @@ -487,6 +506,7 @@ impl<'a> Changelog<'a> { /// If no Azure DevOps related variable is used in the template then this /// function returns empty vectors. #[cfg(feature = "azure_devops")] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] fn get_azure_devops_metadata( &self, ref_name: Option<&str>, @@ -502,12 +522,7 @@ impl<'a> Changelog<'a> { { let azure_devops_client = AzureDevOpsClient::try_from(self.config.remote.azure_devops.clone())?; - log::info!( - "{} ({})", - azure_devops::START_FETCHING_MSG, - self.config.remote.azure_devops - ); - let data = tokio::runtime::Builder::new_multi_thread() + tokio::runtime::Builder::new_multi_thread() .enable_all() .build()? .block_on(async { @@ -521,9 +536,7 @@ impl<'a> Changelog<'a> { pull_requests.len() ); Ok((commits, pull_requests)) - }); - log::info!("{}", azure_devops::FINISHED_FETCHING_MSG); - data + }) } else { Ok((vec![], vec![])) } @@ -540,6 +553,7 @@ impl<'a> Changelog<'a> { /// Adds remote data (e.g. GitHub commits) to the releases. #[allow(unused_variables)] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub fn add_remote_data(&mut self, range: Option<&str>) -> Result<()> { log::debug!("Adding remote data"); self.add_remote_context()?; @@ -611,6 +625,7 @@ impl<'a> Changelog<'a> { } /// Increments the version for the unreleased changes based on semver. + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub fn bump_version(&mut self) -> Result> { if let Some(ref mut last_release) = self.releases.iter_mut().next() { if last_release.version.is_none() { @@ -631,6 +646,7 @@ impl<'a> Changelog<'a> { } /// Generates the changelog and writes it to the given output. + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub fn generate(&self, out: &mut W) -> Result<()> { log::debug!("Generating changelog"); let postprocessors = self.config.changelog.postprocessors.clone(); @@ -694,6 +710,7 @@ impl<'a> Changelog<'a> { } /// Generates a changelog and prepends it to the given changelog. + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub fn prepend(&self, mut changelog: String, out: &mut W) -> Result<()> { log::debug!("Generating changelog and prepending"); if let Some(header) = &self.config.changelog.header { @@ -705,6 +722,7 @@ impl<'a> Changelog<'a> { } /// Prints the changelog context to the given output. + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub fn write_context(&self, out: &mut W) -> Result<()> { let output = Releases { releases: &self.releases, diff --git a/git-cliff-core/src/commit.rs b/git-cliff-core/src/commit.rs index d03dbf3633..758b3b9813 100644 --- a/git-cliff-core/src/commit.rs +++ b/git-cliff-core/src/commit.rs @@ -209,6 +209,13 @@ impl Commit<'_> { /// * converts commit to a conventional commit /// * sets the group for the commit /// * extracts links and generates URLs + #[cfg_attr( + feature = "tracing", + tracing::instrument( + skip_all, + fields(id = self.id) + ) + )] pub fn process(&self, config: &GitConfig) -> Result { let mut commit = self.clone(); commit = commit.preprocess(&config.commit_preprocessors)?; @@ -249,6 +256,13 @@ impl Commit<'_> { /// Modifies the commit [`message`] using regex or custom OS command. /// /// [`message`]: Commit::message + #[cfg_attr( + feature = "tracing", + tracing::instrument( + skip_all, + fields(id = self.id) + ) + )] pub fn preprocess(mut self, preprocessors: &[TextProcessor]) -> Result { preprocessors.iter().try_for_each(|preprocessor| { preprocessor.replace(&mut self.message, vec![("COMMIT_SHA", &self.id)])?; @@ -273,6 +287,13 @@ impl Commit<'_> { /// /// [`group`]: Commit::group /// [`scope`]: Commit::scope + #[cfg_attr( + feature = "tracing", + tracing::instrument( + skip_all, + fields(id = self.id) + ) + )] pub fn parse( mut self, parsers: &[CommitParser], @@ -389,6 +410,13 @@ impl Commit<'_> { /// Sets the [`links`] of the commit. /// /// [`links`]: Commit::links + #[cfg_attr( + feature = "tracing", + tracing::instrument( + skip_all, + fields(id = self.id) + ) + )] pub fn parse_links(mut self, parsers: &[LinkParser]) -> Self { for parser in parsers { let regex = &parser.pattern; @@ -498,6 +526,7 @@ impl Serialize for Commit<'_> { /// [`Commit::into_conventional`]. /// /// This function is to be used only in [`crate::release::Release::commits`]. +#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub(crate) fn commits_to_conventional_commits<'de, 'a, D: Deserializer<'de>>( deserializer: D, ) -> std::result::Result>, D::Error> { diff --git a/git-cliff-core/src/release.rs b/git-cliff-core/src/release.rs index dba0ecc306..f5ecf37bbe 100644 --- a/git-cliff-core/src/release.rs +++ b/git-cliff-core/src/release.rs @@ -84,6 +84,16 @@ impl Release<'_> { /// /// It uses the default bump version configuration to calculate the next /// version. + #[cfg_attr( + feature = "tracing", + tracing::instrument( + skip_all, + fields( + version = self.version.as_deref().unwrap_or("unreleased"), + commits = self.commits.len() + ) + ) + )] pub fn calculate_next_version(&self) -> Result { self.calculate_next_version_with_config(&Bump::default()) } @@ -102,6 +112,16 @@ impl Release<'_> { /// /// It uses the given bump version configuration to calculate the next /// version. + #[cfg_attr( + feature = "tracing", + tracing::instrument( + skip_all, + fields( + version = self.version.as_deref().unwrap_or("unreleased"), + commits = self.commits.len() + ) + ) + )] pub(super) fn calculate_next_version_with_config(&self, config: &Bump) -> Result { match self .previous diff --git a/git-cliff-core/src/remote/azure_devops.rs b/git-cliff-core/src/remote/azure_devops.rs index 691a86adf5..19646149e2 100644 --- a/git-cliff-core/src/remote/azure_devops.rs +++ b/git-cliff-core/src/remote/azure_devops.rs @@ -7,12 +7,6 @@ use super::*; use crate::config::Remote; use crate::error::*; -/// Log message to show while fetching data from Azure DevOps. -pub const START_FETCHING_MSG: &str = "Retrieving data from Azure DevOps..."; - -/// Log message to show when done fetching from Azure DevOps. -pub const FINISHED_FETCHING_MSG: &str = "Done fetching Azure DevOps data."; - /// Template variables related to this remote. pub(crate) const TEMPLATE_VARIABLES: &[&str] = &[ "azure_devops", @@ -225,6 +219,7 @@ impl AzureDevOpsClient { /// Fetches the complete list of commits. /// This is inefficient for large repositories; consider using /// `get_commit_stream` instead. + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn get_commits(&self, ref_name: Option<&str>) -> Result>> { use futures::TryStreamExt; self.get_commit_stream(ref_name).try_collect().await @@ -233,6 +228,7 @@ impl AzureDevOpsClient { /// Fetches the complete list of pull requests. /// This is inefficient for large repositories; consider using /// `get_pull_request_stream` instead. + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn get_pull_requests(&self) -> Result>> { use futures::TryStreamExt; self.get_pull_request_stream().try_collect().await diff --git a/git-cliff-core/src/remote/bitbucket.rs b/git-cliff-core/src/remote/bitbucket.rs index 3b020eae01..5927a2fb4b 100644 --- a/git-cliff-core/src/remote/bitbucket.rs +++ b/git-cliff-core/src/remote/bitbucket.rs @@ -7,12 +7,6 @@ use super::*; use crate::config::Remote; use crate::error::*; -/// Log message to show while fetching data from Bitbucket. -pub const START_FETCHING_MSG: &str = "Retrieving data from Bitbucket..."; - -/// Log message to show when done fetching 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"]; @@ -177,6 +171,7 @@ impl BitbucketClient { /// Fetches the complete list of commits. /// This is inefficient for large repositories; consider using /// `get_commit_stream` instead. + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn get_commits(&self, ref_name: Option<&str>) -> Result>> { use futures::TryStreamExt; @@ -186,6 +181,7 @@ impl BitbucketClient { /// Fetches the complete list of pull requests. /// This is inefficient for large repositories; consider using /// `get_pull_request_stream` instead. + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn get_pull_requests(&self) -> Result>> { use futures::TryStreamExt; diff --git a/git-cliff-core/src/remote/gitea.rs b/git-cliff-core/src/remote/gitea.rs index 5b1b2d30b0..5beb846988 100644 --- a/git-cliff-core/src/remote/gitea.rs +++ b/git-cliff-core/src/remote/gitea.rs @@ -7,12 +7,6 @@ use super::*; use crate::config::Remote; use crate::error::*; -/// Log message to show while fetching data from Gitea. -pub const START_FETCHING_MSG: &str = "Retrieving data from Gitea..."; - -/// Log message to show when done fetching 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"]; @@ -146,6 +140,7 @@ impl GiteaClient { /// Fetches the complete list of commits. /// This is inefficient for large repositories; consider using /// `get_commit_stream` instead. + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn get_commits(&self, ref_name: Option<&str>) -> Result>> { use futures::TryStreamExt; self.get_commit_stream(ref_name).try_collect().await @@ -154,6 +149,7 @@ impl GiteaClient { /// Fetches the complete list of pull requests. /// This is inefficient for large repositories; consider using /// `get_pull_request_stream` instead. + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn get_pull_requests(&self) -> Result>> { use futures::TryStreamExt; self.get_pull_request_stream().try_collect().await diff --git a/git-cliff-core/src/remote/github.rs b/git-cliff-core/src/remote/github.rs index 25e32724a7..9bd5c154e6 100644 --- a/git-cliff-core/src/remote/github.rs +++ b/git-cliff-core/src/remote/github.rs @@ -7,12 +7,6 @@ use super::*; use crate::config::Remote; use crate::error::*; -/// Log message to show while fetching data from GitHub. -pub const START_FETCHING_MSG: &str = "Retrieving data from GitHub..."; - -/// Log message to show when done fetching 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"]; @@ -162,6 +156,7 @@ impl GitHubClient { /// Fetches the complete list of commits. /// This is inefficient for large repositories; consider using /// `get_commit_stream` instead. + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn get_commits(&self, ref_name: Option<&str>) -> Result>> { use futures::TryStreamExt; self.get_commit_stream(ref_name).try_collect().await @@ -170,6 +165,7 @@ impl GitHubClient { /// Fetches the complete list of pull requests. /// This is inefficient for large repositories; consider using /// `get_pull_request_stream` instead. + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn get_pull_requests(&self) -> Result>> { use futures::TryStreamExt; self.get_pull_request_stream().try_collect().await diff --git a/git-cliff-core/src/remote/gitlab.rs b/git-cliff-core/src/remote/gitlab.rs index 5e40d9eb03..6c64e7b795 100644 --- a/git-cliff-core/src/remote/gitlab.rs +++ b/git-cliff-core/src/remote/gitlab.rs @@ -7,12 +7,6 @@ use super::*; use crate::config::Remote; use crate::error::*; -/// Log message to show while fetching data from GitLab. -pub const START_FETCHING_MSG: &str = "Retrieving data from GitLab..."; - -/// Log message to show when done fetching 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"]; @@ -233,6 +227,7 @@ impl GitLabClient { } /// Looks up the project details. + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn get_project(&self) -> Result { let url = Self::project_url(&self.api_url(), &self.remote()); self.get_json::(&url).await @@ -241,6 +236,7 @@ impl GitLabClient { /// Fetches the complete list of commits. /// This is inefficient for large repositories; consider using /// `get_commit_stream` instead. + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn get_commits( &self, project_id: i64, @@ -255,6 +251,7 @@ impl GitLabClient { /// Fetches the complete list of pull requests. /// This is inefficient for large repositories; consider using /// `get_pull_request_stream` instead. + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub async fn get_pull_requests( &self, project_id: i64, diff --git a/git-cliff/Cargo.toml b/git-cliff/Cargo.toml index f9dd681e21..af2bc95f2d 100644 --- a/git-cliff/Cargo.toml +++ b/git-cliff/Cargo.toml @@ -29,7 +29,7 @@ integrations = ["github", "gitlab", "gitea", "bitbucket", "azure_devops"] # inform about new releases update-informer = ["dep:update-informer"] # enable remote repository integration -remote = ["dep:indicatif", "dep:reqwest"] +remote = ["dep:reqwest"] # enable GitHub integration github = ["remote", "git-cliff-core/github"] # enable GitLab integration @@ -56,16 +56,20 @@ clap_complete = "4.5.61" clap_mangen = "0.2.31" shellexpand = "3.1.0" update-informer = { version = "1.3.0", optional = true } -indicatif = { version = "0.18.3", optional = true } -env_logger = "=0.10.2" +indicatif = "0.18.3" pprof = { version = "0.15.0", optional = true } rand = { version = "0.9.2", optional = true } reqwest = { workspace = true, optional = true } url.workspace = true pathdiff = "0.2.3" +tracing = { workspace = true, features = ["log"] } +tracing-subscriber = { version = "0.3.22", features = ["env-filter", "json"] } +tracing-indicatif = "0.3.14" +owo-colors = "4.2.3" [dependencies.git-cliff-core] version = "2.11.0" # managed by release.sh +features = ["tracing"] path = "../git-cliff-core" [dev-dependencies] diff --git a/git-cliff/src/logger.rs b/git-cliff/src/logger.rs index 6e4e0a29e5..9404c92951 100644 --- a/git-cliff/src/logger.rs +++ b/git-cliff/src/logger.rs @@ -1,20 +1,24 @@ -use std::io::Write; +use std::fmt; use std::sync::atomic::{AtomicUsize, Ordering}; -use std::{env, fmt}; -use env_logger::Builder; -use env_logger::fmt::{Color, Style, StyledValue}; use git_cliff_core::error::{Error, Result}; -#[cfg(feature = "remote")] -use indicatif::{ProgressBar, ProgressStyle}; -use log::Level; - -/// Environment variable to use for the logger. -const LOGGER_ENV: &str = "RUST_LOG"; +use indicatif::{ProgressState, ProgressStyle}; +use owo_colors::{OwoColorize, Style, Styled}; +use tracing::{Event, Level, Subscriber}; +use tracing_indicatif::IndicatifLayer; +use tracing_subscriber::fmt::FmtContext; +use tracing_subscriber::fmt::format::{self, FormatEvent, FormatFields}; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::registry::LookupSpan; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::{EnvFilter, Registry}; /// Global variable for storing the maximum width of the modules. static MAX_MODULE_WIDTH: AtomicUsize = AtomicUsize::new(0); +/// Unicode braille spinner frames used by indicatif. +const SPINNER_TICKS: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; + /// Wrapper for the padded values. struct Padded { value: T, @@ -38,120 +42,162 @@ fn max_target_width(target: &str) -> usize { } } -/// 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"), +/// Adds styles/colors to the given level and returns it. +fn style_level(level: &Level) -> Styled<&'static str> { + match *level { + Level::ERROR => Style::new().red().bold().style("ERROR"), + Level::WARN => Style::new().yellow().bold().style("WARN"), + Level::INFO => Style::new().green().bold().style("INFO"), + Level::DEBUG => Style::new().blue().bold().style("DEBUG"), + Level::TRACE => Style::new().magenta().bold().style("TRACE"), + } +} + +/// Shortens the target string to fit within the specified width. +/// TODO: This function is currently unused but kept for future. +#[allow(dead_code)] +fn shorten_target(target: &str, width: usize) -> String { + if target.len() <= width { + return target.to_string(); + } + let parts: Vec<&str> = target.split("::").collect(); + if parts.len() >= 2 { + format!("{}...{}", parts[0], parts[parts.len() - 1]) + } else { + target.to_string() } } -#[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 +/// Formats the elapsed time as `X.Ys` (sub-second precision). +fn elapsed_subsec_key(state: &ProgressState, writer: &mut dyn fmt::Write) { + let seconds = state.elapsed().as_secs(); + let sub_seconds = (state.elapsed().as_millis() % 1000) / 100; + let _ = write!(writer, "{}.{}s", seconds, sub_seconds); +} + +/// Emits an ANSI color escape sequence for the spinner, based on elapsed time. +/// +/// The color gradually transitions: +/// - green -> yellow (0–4s) +/// - yellow -> red (4–8s) +fn color_start_key(state: &ProgressState, writer: &mut dyn fmt::Write) { + let elapsed = state.elapsed().as_secs_f32(); + let t = (elapsed / 8.0).min(1.0); // 8秒で変化 + let (r, g, b) = if t < 0.5 { + let nt = t * 2.0; + (lerp(140, 230, nt), lerp(200, 210, nt), lerp(160, 150, nt)) + } else { + let nt = (t - 0.5) * 2.0; + (lerp(230, 230, nt), lerp(210, 140, nt), lerp(150, 140, nt)) }; + let _ = write!(writer, "\x1b[38;2;{};{};{}m", r, g, b); +} + +/// Performs linear interpolation between two color components. +fn lerp(a: u8, b: u8, t: f32) -> u8 { + ((a as f32 + (b as f32 - a as f32) * t).clamp(0.0, 255.0)) as u8 } -/// Initializes the global logger. +/// Resets ANSI styling to the terminal default. /// -/// This method also creates a progress bar which is triggered -/// 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); +/// This must be paired with `color_start_key` to avoid leaking color state into subsequent log +/// output. +fn color_end_key(_: &ProgressState, writer: &mut dyn fmt::Write) { + let _ = writer.write_str("\x1b[0m"); +} - 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, - }); - - #[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(()); - } - } +/// Emits an ANSI escape sequence for dim (muted) foreground text. +fn dim_start_key(_: &ProgressState, writer: &mut dyn fmt::Write) { + let _ = writer.write_str("\x1b[90m"); +} - #[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(()); - } - } +/// Resets ANSI styling to the terminal default. +/// +/// This must be paired with `dim_start_key` to avoid leaking color state into subsequent log +/// output. +fn dim_end_key(_: &ProgressState, writer: &mut dyn fmt::Write) { + let _ = writer.write_str("\x1b[0m"); +} - #[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(()); - } - } +/// Builds the `indicatif::ProgressStyle` used for tracing spans. +/// +/// This style: +/// - renders a Unicode spinner for active spans +/// - colorizes the spinner based on elapsed time +/// - shows span name, fields, and wide messages +/// - appends a sub-second elapsed timer +fn indicatif_progress_style() -> ProgressStyle { + ProgressStyle::with_template( + "{span_child_prefix}{color_start}{spinner}{color_end} {dim_start}{span_name} \ + {span_fields} {wide_msg}{dim_end} [{color_start}{elapsed_subsec}{color_end}]", + ) + .unwrap() + .with_key("elapsed_subsec", elapsed_subsec_key) + .with_key("color_start", color_start_key) + .with_key("color_end", color_end_key) + .with_key("dim_start", dim_start_key) + .with_key("dim_end", dim_end_key) + .tick_strings(SPINNER_TICKS) +} - #[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(()); +/// Simple formatter. We format: "LEVEL TARGET > MESSAGE", with a basic padding for target. +struct GitCliffFormatter; + +impl FormatEvent for GitCliffFormatter +where + S: Subscriber + for<'a> LookupSpan<'a>, + N: for<'a> FormatFields<'a> + 'static, +{ + fn format_event( + &self, + ctx: &FmtContext<'_, S, N>, + mut writer: format::Writer<'_>, + event: &Event<'_>, + ) -> fmt::Result { + let metadata = event.metadata(); + let level = style_level(metadata.level()); + let target = metadata.target(); + let max_width = max_target_width(target); + write!( + &mut writer, + "{} {} > ", + Padded { + value: level, + width: 5, + }, + Padded { + value: target.bright_black().bold(), + width: max_width, + }, + )?; + if let Some(scope) = ctx.event_scope() { + for span in scope.from_root() { + write!(writer, "{}", span.name().bright_black().bold())?; + write!(writer, "{}", ": ".bright_black().bold())?; } } - - writeln!(f, " {} {} > {}", level, target, record.args()) - }); - - if let Ok(var) = env::var(LOGGER_ENV) { - builder.parse_filters(&var); + ctx.field_format().format_fields(writer.by_ref(), event)?; + writeln!(writer) } +} - builder +/// Initializes the global tracing subscriber. +pub fn init() -> Result<()> { + // Build EnvFilter from `RUST_LOG` or fallback to "info" + let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()); + let indicatif_layer = IndicatifLayer::new() + .with_progress_style(indicatif_progress_style()) + .with_span_child_prefix_symbol("↳ ") + .with_span_child_prefix_indent(" "); + let fmt_layer = tracing_subscriber::fmt::layer() + .with_writer(indicatif_layer.get_stderr_writer()) + .with_ansi(true) + .event_format(GitCliffFormatter); + let subscriber = Registry::default() + .with(env_filter) + .with(indicatif_layer) + .with(fmt_layer); + subscriber .try_init() .map_err(|e| Error::LoggerError(e.to_string())) }