use super::{Context, Module, RootModuleConfig}; use git2::Repository; use git2::Time; use crate::configs::git_commit::GitCommitConfig; use crate::formatter::StringFormatter; /// Creates a module with the Git commit in the current directory /// /// Will display the commit hash if the current directory is a git repo pub fn module<'a>(context: &'a Context) -> Option> { let mut module = context.new_module("git_commit"); let config: GitCommitConfig = GitCommitConfig::try_load(module.config); let repo = context.get_repo().ok()?; let repo_root = repo.root.as_ref()?; let git_repo = Repository::open(repo_root).ok()?; let is_detached = git_repo.head_detached().ok()?; if config.only_detached && !is_detached { return None; }; let git_head = git_repo.head().ok()?; let head_commit = git_head.peel_to_commit().ok()?; let commit_oid = head_commit.id(); let mut parsed; parsed = StringFormatter::new(config.format).and_then(|formatter| { formatter .map_style(|variable| match variable { "style" => Some(Ok(config.style)), _ => None, }) .map(|variable| match variable { "hash" => Some(Ok(id_to_hex_abbrev( commit_oid.as_bytes(), config.commit_hash_length, ))), _ => None, }) .parse(None) }); if !config.tag_disabled { // Let's get repo tags names let tag_names = git_repo.tag_names(None).ok()?; let tag_and_refs = tag_names.iter().flatten().flat_map(|name| { let full_tag = format!("refs/tags/{}", name); let tag_obj = git_repo.find_reference(&full_tag)?.peel_to_tag()?; let sig_obj = tag_obj.tagger(); git_repo.find_reference(&full_tag).map(|reference| { ( String::from(name), // fall back to oldest + 1s time if sig_obj is unavailable sig_obj.map_or(git2::Time::new(1, 0), |s| s.when()), reference, ) }) }); let mut tag_name = String::new(); let mut oldest = Time::new(0, 0); // Let's check if HEAD has some tag. If several, gets last created one... for (name, timestamp, reference) in tag_and_refs.rev() { if commit_oid == reference.peel_to_commit().ok()?.id() && timestamp > oldest { tag_name = name; oldest = timestamp; } } // If we have tag... if !tag_name.is_empty() { parsed = StringFormatter::new(config.format).and_then(|formatter| { formatter .map_style(|variable| match variable { "style" => Some(Ok(config.style)), _ => None, }) .map(|variable| match variable { "hash" => Some(Ok(id_to_hex_abbrev( commit_oid.as_bytes(), config.commit_hash_length, ))), _ => None, }) .map(|variable| match variable { "tag" => Some(Ok(format!("{}{}", &config.tag_symbol, &tag_name))), _ => None, }) .parse(None) }); } }; module.set_segments(match parsed { Ok(segments) => segments, Err(error) => { log::warn!("Error in module `git_commit`:\n{}", error); return None; } }); Some(module) } /// len specifies length of hex encoded string pub fn id_to_hex_abbrev(bytes: &[u8], len: usize) -> String { bytes .iter() .map(|b| format!("{:02x}", b)) .collect::>() .join("") .chars() .take(len) .collect() } #[cfg(test)] mod tests { use ansi_term::Color; use std::process::Command; use std::{io, str}; use crate::test::{fixture_repo, FixtureProvider, ModuleRenderer}; #[test] fn show_nothing_on_empty_dir() -> io::Result<()> { let repo_dir = tempfile::tempdir()?; let actual = ModuleRenderer::new("git_commit") .path(&repo_dir.path()) .collect(); let expected = None; assert_eq!(expected, actual); repo_dir.close() } #[test] fn test_render_commit_hash() -> io::Result<()> { let repo_dir = fixture_repo(FixtureProvider::Git)?; let mut git_output = Command::new("git") .args(&["rev-parse", "HEAD"]) .current_dir(&repo_dir.path()) .output()? .stdout; git_output.truncate(7); let expected_hash = str::from_utf8(&git_output).unwrap(); let actual = ModuleRenderer::new("git_commit") .config(toml::toml! { [git_commit] only_detached = false }) .path(&repo_dir.path()) .collect(); let expected = Some(format!( "{} ", Color::Green .bold() .paint(format!("({})", expected_hash)) .to_string() )); assert_eq!(expected, actual); repo_dir.close() } #[test] fn test_render_commit_hash_len_override() -> io::Result<()> { let repo_dir = fixture_repo(FixtureProvider::Git)?; let mut git_output = Command::new("git") .args(&["rev-parse", "HEAD"]) .current_dir(&repo_dir.path()) .output()? .stdout; git_output.truncate(14); let expected_hash = str::from_utf8(&git_output).unwrap(); let actual = ModuleRenderer::new("git_commit") .config(toml::toml! { [git_commit] only_detached = false commit_hash_length = 14 }) .path(&repo_dir.path()) .collect(); let expected = Some(format!( "{} ", Color::Green .bold() .paint(format!("({})", expected_hash)) .to_string() )); assert_eq!(expected, actual); repo_dir.close() } #[test] fn test_render_commit_hash_only_detached_on_branch() -> io::Result<()> { let repo_dir = fixture_repo(FixtureProvider::Git)?; let actual = ModuleRenderer::new("git_commit") .path(&repo_dir.path()) .collect(); let expected = None; assert_eq!(expected, actual); repo_dir.close() } #[test] fn test_render_commit_hash_only_detached_on_detached() -> io::Result<()> { let repo_dir = fixture_repo(FixtureProvider::Git)?; Command::new("git") .args(&["checkout", "@~1"]) .current_dir(&repo_dir.path()) .output()?; let mut git_output = Command::new("git") .args(&["rev-parse", "HEAD"]) .current_dir(&repo_dir.path()) .output()? .stdout; git_output.truncate(7); let expected_hash = str::from_utf8(&git_output).unwrap(); let actual = ModuleRenderer::new("git_commit") .path(&repo_dir.path()) .collect(); let expected = Some(format!( "{} ", Color::Green .bold() .paint(format!("({})", expected_hash)) .to_string() )); assert_eq!(expected, actual); repo_dir.close() } #[test] fn test_render_commit_hash_with_tag_enabled() -> io::Result<()> { let repo_dir = fixture_repo(FixtureProvider::Git)?; let mut git_commit = Command::new("git") .args(&["rev-parse", "HEAD"]) .current_dir(&repo_dir.path()) .output()? .stdout; git_commit.truncate(7); let commit_output = str::from_utf8(&git_commit).unwrap().trim(); let git_tag = Command::new("git") .args(&["describe", "--tags", "--exact-match", "HEAD"]) .current_dir(&repo_dir.path()) .output()? .stdout; let tag_output = str::from_utf8(&git_tag).unwrap().trim(); let expected_output = format!("{} {}", commit_output, tag_output); let actual = ModuleRenderer::new("git_commit") .config(toml::toml! { [git_commit] only_detached = false tag_disabled = false tag_symbol = "" }) .path(&repo_dir.path()) .collect(); let expected = Some(format!( "{} ", Color::Green .bold() .paint(format!("({})", expected_output.trim())) .to_string() )); assert_eq!(expected, actual); Ok(()) } #[test] fn test_render_commit_hash_only_detached_on_detached_with_tag_enabled() -> io::Result<()> { let repo_dir = fixture_repo(FixtureProvider::Git)?; Command::new("git") .args(&["checkout", "@~1"]) .current_dir(&repo_dir.path()) .output()?; Command::new("git") .args(&["tag", "tagOnDetached", "-m", "Testing tags on detached"]) .current_dir(&repo_dir.path()) .output()?; let mut git_commit = Command::new("git") .args(&["rev-parse", "HEAD"]) .current_dir(&repo_dir.path()) .output()? .stdout; git_commit.truncate(7); let commit_output = str::from_utf8(&git_commit).unwrap().trim(); let git_tag = Command::new("git") .args(&["describe", "--tags", "--exact-match", "HEAD"]) .current_dir(&repo_dir.path()) .output()? .stdout; let tag_output = str::from_utf8(&git_tag).unwrap().trim(); let expected_output = format!("{} {}", commit_output, tag_output); let actual = ModuleRenderer::new("git_commit") .config(toml::toml! { [git_commit] tag_disabled = false tag_symbol = " " }) .path(&repo_dir.path()) .collect(); let expected = Some(format!( "{} ", Color::Green .bold() .paint(format!("({})", expected_output.trim())) .to_string() )); assert_eq!(expected, actual); Ok(()) } #[test] fn test_latest_tag_shown_with_tag_enabled() -> io::Result<()> { use std::{thread, time}; let repo_dir = fixture_repo(FixtureProvider::Git)?; let mut git_commit = Command::new("git") .args(&["rev-parse", "HEAD"]) .current_dir(&repo_dir.path()) .output()? .stdout; git_commit.truncate(7); let commit_output = str::from_utf8(&git_commit).unwrap().trim(); Command::new("git") .args(&["tag", "v2", "-m", "Testing tags v2"]) .current_dir(&repo_dir.path()) .output()?; // Wait one second between tags thread::sleep(time::Duration::from_millis(1000)); Command::new("git") .args(&["tag", "v0", "-m", "Testing tags v0", "HEAD~1"]) .current_dir(&repo_dir.path()) .output()?; Command::new("git") .args(&["tag", "v1", "-m", "Testing tags v1"]) .current_dir(&repo_dir.path()) .output()?; let git_tag = Command::new("git") .args(&[ "for-each-ref", "--contains", "HEAD", "--sort=-taggerdate", "--count=1", "--format", "%(refname:short)", "refs/tags", ]) .current_dir(&repo_dir.path()) .output()? .stdout; let tag_output = str::from_utf8(&git_tag).unwrap().trim(); let expected_output = format!("{} {}", commit_output, tag_output); let actual = ModuleRenderer::new("git_commit") .config(toml::toml! { [git_commit] only_detached = false tag_disabled = false tag_symbol = " " }) .path(&repo_dir.path()) .collect(); let expected = Some(format!( "{} ", Color::Green .bold() .paint(format!("({})", expected_output.trim())) .to_string() )); assert_eq!(expected, actual); Ok(()) } }