use std::{
path::{Path, PathBuf},
str::FromStr,
time::{Duration, SystemTime},
};
use chrono::{DateTime, Local};
use git2::Repository;
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum TagKind {
Todo,
TodoMacro,
Bug,
Fix,
Note,
Undone,
Hack,
Xxx,
Optimize,
Safety,
Invariant,
Lint,
Ignored,
Custom(String),
}
impl TagKind {
pub fn new(tag: &str) -> Self {
let Ok(tag) = Self::from_str(tag) else {
return Self::Custom(tag.to_owned());
};
tag
}
pub fn level(&self) -> TagLevel {
match self {
Self::Todo | Self::TodoMacro => TagLevel::Improvement,
Self::Bug | Self::Fix => TagLevel::Fix,
Self::Note
| Self::Undone
| Self::Hack
| Self::Xxx
| Self::Optimize
| Self::Safety
| Self::Invariant
| Self::Lint
| Self::Ignored => TagLevel::Information,
Self::Custom(_) => TagLevel::Custom,
}
}
#[cfg(feature = "color")]
pub fn color(&self) -> crossterm::style::Color {
match self {
Self::TodoMacro => crossterm::style::Color::Magenta,
_ => self.level().color(),
}
}
}
#[derive(Debug)]
pub struct UnknownTagKind;
impl std::fmt::Display for UnknownTagKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Unknown tag kind")
}
}
impl std::error::Error for UnknownTagKind {}
impl FromStr for TagKind {
type Err = UnknownTagKind;
fn from_str(tag: &str) -> Result<Self, Self::Err> {
let lowercase_tag = tag.to_lowercase();
match lowercase_tag.as_str() {
"todo" => Ok(Self::Todo),
"todo!" => Ok(Self::TodoMacro),
"bug" | "debug" => Ok(Self::Bug),
"fixme" | "fix" => Ok(Self::Fix),
"note" | "nb" => Ok(Self::Note),
"undone" => Ok(Self::Undone),
"hack" | "bodge" | "kludge" => Ok(Self::Hack),
"xxx" => Ok(Self::Xxx),
"optimize" | "optimise" | "optimizeme" | "optimiseme" => Ok(Self::Optimize),
"safety" => Ok(Self::Safety),
"invariant" => Ok(Self::Invariant),
"lint" => Ok(Self::Lint),
"ignored" => Ok(Self::Ignored),
_ => Err(UnknownTagKind),
}
}
}
impl std::fmt::Display for TagKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Todo => "TODO",
Self::TodoMacro => "TODO!",
Self::Bug => "BUG",
Self::Fix => "FIX",
Self::Note => "NOTE",
Self::Undone => "UNDONE",
Self::Hack => "HACK",
Self::Xxx => "XXX",
Self::Optimize => "OPTIMIZE",
Self::Safety => "SAFETY",
Self::Invariant => "INVARIANT",
Self::Lint => "LINT",
Self::Ignored => "IGNORED",
Self::Custom(custom) => custom,
}
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TagLevel {
Fix,
Improvement,
Information,
Custom,
}
impl TagLevel {
#[cfg(feature = "color")]
pub fn color(&self) -> crossterm::style::Color {
match self {
Self::Fix => crossterm::style::Color::Red,
Self::Improvement => crossterm::style::Color::Blue,
Self::Information => crossterm::style::Color::Grey,
Self::Custom => crossterm::style::Color::Yellow,
}
}
}
impl std::fmt::Display for TagLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Fix => "Fix",
Self::Improvement => "Improvement",
Self::Information => "Information",
Self::Custom => "Custom",
}
)
}
}
#[derive(Debug)]
pub struct UnknownTagLevel;
impl std::fmt::Display for UnknownTagLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Unknown tag level")
}
}
impl std::error::Error for UnknownTagLevel {}
impl FromStr for TagLevel {
type Err = UnknownTagLevel;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"fix" => Ok(Self::Fix),
"improvement" => Ok(Self::Improvement),
"information" => Ok(Self::Information),
"custom" => Ok(Self::Custom),
_ => Err(UnknownTagLevel),
}
}
}
#[derive(Debug, Serialize)]
pub struct Tag {
pub path: PathBuf,
pub line: usize,
pub kind: TagKind,
pub message: String,
pub git_info: Option<GitInfo>,
}
impl std::fmt::Display for Tag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(git_info) = &self.git_info {
write!(
f,
"{}: {} {} {}:{}",
self.kind,
self.message,
git_info,
self.path.display(),
self.line
)
} else {
write!(
f,
"{}: {} {}:{}",
self.kind,
self.message,
self.path.display(),
self.line,
)
}
}
}
impl Tag {
pub fn get_blame_info(&self, repo_path: &Path, repo: &Repository) -> Option<GitInfo> {
let path = self.path.strip_prefix(repo_path).ok()?;
let blame = repo.blame_file(path, Default::default()).ok()?;
let blame_hunk = blame.get_line(self.line)?;
let commit = repo.find_commit(blame_hunk.final_commit_id()).ok()?;
let seconds = commit.time().seconds();
let duration = Duration::from_secs(u64::try_from(seconds).ok()?);
let git_info = GitInfo {
time: SystemTime::UNIX_EPOCH + duration,
author: commit.author().name()?.to_owned(),
};
Some(git_info)
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
pub struct GitInfo {
pub time: SystemTime,
pub author: String,
}
impl std::fmt::Display for GitInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let time: DateTime<Local> = self.time.into();
write!(f, "{} {}", time.format("%F %T"), self.author)
}
}