pub mod updater;
mod v0_1;
mod v0_2;
pub use v0_2::{Config, Scopes, Templates, Ticket};
use std::{fs, io, path::PathBuf, process::Command};
use indexmap::{IndexMap, indexmap};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::tracing::LogResult as _;
#[derive(Debug, Error)]
pub enum LoadError {
#[error("Failed to get the configuration file path")]
ConfigFileError(#[from] ConfigFileError),
#[error("Failed to read {CONFIG_FILE_NAME}")]
ReadError(#[source] io::Error),
#[error("Invalid configuration in {CONFIG_FILE_NAME}")]
InvalidConfig(#[from] FromTomlError),
}
#[derive(Debug, Error)]
pub enum FromTomlError {
#[error("Unsupported configuration version {version}")]
UnsupportedVersion {
version: String,
},
#[error("Unsupported development configuration version {version}")]
UnsupportedDevelopmentVersion {
version: String,
gitz_version: String,
},
#[error("Failed to parse into a valid configuration")]
ParseError(#[source] toml::de::Error),
}
#[derive(Debug, Error)]
pub enum ConfigFileError {
#[error("Failed to get the Git repo root")]
RepoRootError(#[from] RepoRootError),
}
#[derive(Debug, Error)]
pub enum RepoRootError {
#[error("Failed to run the git command")]
CannotRunGit(#[source] io::Error),
#[error("{0}")]
GitError(String),
#[error("The output of the git command is not proper UTF-8")]
EncodingError(#[source] std::string::FromUtf8Error),
}
#[derive(Debug, Serialize, Deserialize)]
struct MinimalConfig {
version: String,
}
pub const CONFIG_FILE_NAME: &str = "git-z.toml";
pub const VERSION: &str = "0.2";
const DEFAULT_TEMPLATE: &str = include_str!("../templates/COMMIT_EDITMSG");
impl Default for Config {
fn default() -> Self {
let default_types = indexmap! {
"feat" => "add a new feature in the code (including tests for the feature)",
"sec" => "patch a security issue (including updating a dependency for security)",
"fix" => "patch a bug in the code",
"perf" => "enhance the performance of the code",
"refactor" => "restructure the code without changing its external behaviour",
"test" => "add, update (including refactoring) or remove tests only",
"docs" => "update the documentation only (including README and alike)",
"style" => "update the style, like running a code formatter or changing headers",
"deps" => "add, update or remove external dependencies used by the code",
"build" => "update the toolchain, build scripts or package definitions",
"env" => "update the development environment",
"ide" => "update the IDE configuration",
"ci" => "update the CI configuration (including local check scripts)",
"revert" => "revert a previous commit",
"chore" => "update or remove something that is not covered by any other type",
"wip" => "work in progress / to be rebased and squashed later",
"debug" => "commit used for debugging purposes, not to be integrated",
};
Self {
version: String::from(VERSION),
types: default_types
.into_iter()
.map(|(key, value)| (String::from(key), String::from(value)))
.collect(),
scopes: Some(Scopes::Any),
ticket: None,
templates: Templates {
commit: String::from(DEFAULT_TEMPLATE),
},
}
}
}
impl Config {
#[tracing::instrument(name = "load_config", level = "trace")]
pub fn load() -> Result<Self, LoadError> {
let config_file = config_file()?;
match fs::read_to_string(&config_file) {
Ok(config) => {
tracing::info!(?config_file, "loading the configuration");
let config = Self::from_toml(&config)?;
tracing::debug!(?config);
Ok(config)
}
Err(error) => {
if error.kind() == io::ErrorKind::NotFound {
tracing::info!(
"no configuration file, using the default config"
);
Ok(Self::default())
} else {
tracing::error!(
?error,
?config_file,
"cannot read the configuration file",
);
Err(LoadError::ReadError(error))
}
}
}
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn from_toml(toml: &str) -> Result<Self, FromTomlError> {
let minimal_config: MinimalConfig = toml::from_str(toml)
.map_err(FromTomlError::ParseError)
.log_err()?;
match minimal_config.version.as_str() {
VERSION => {
let config = toml::from_str(toml)
.map_err(FromTomlError::ParseError)
.log_err()?;
Ok(config)
}
"0.1" => {
let config: v0_1::Config = toml::from_str(toml)
.map_err(FromTomlError::ParseError)
.log_err()?;
Ok(config.into())
}
version @ ("0.2-dev.0" | "0.2-dev.1" | "0.2-dev.2"
| "0.2-dev.3") => {
Err(FromTomlError::UnsupportedDevelopmentVersion {
version: version.to_owned(),
gitz_version: String::from("0.2.0"),
})
.log_err()
}
version => Err(FromTomlError::UnsupportedVersion {
version: version.to_owned(),
})
.log_err(),
}
}
}
pub fn config_file() -> Result<PathBuf, ConfigFileError> {
Ok(repo_root()?.join(CONFIG_FILE_NAME))
}
#[tracing::instrument(level = "trace")]
fn repo_root() -> Result<PathBuf, RepoRootError> {
let git_rev_parse = Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.output()
.map_err(RepoRootError::CannotRunGit)
.log_err()?;
if git_rev_parse.status.success() {
Ok(String::from_utf8(git_rev_parse.stdout)
.map_err(RepoRootError::EncodingError)
.log_err()?
.trim()
.into())
} else {
Err(RepoRootError::GitError(
String::from_utf8(git_rev_parse.stderr)
.map_err(RepoRootError::EncodingError)
.log_err()?
.trim()
.to_owned(),
))
}
}
impl From<v0_1::Config> for Config {
fn from(old: v0_1::Config) -> Self {
Self {
version: old.version,
types: split_types_and_docs(&old.types),
scopes: Some(Scopes::List { list: old.scopes }),
ticket: Some(Ticket {
required: true,
prefixes: old.ticket_prefixes,
}),
templates: Templates {
commit: old.template,
},
}
}
}
fn split_types_and_docs(types: &[String]) -> IndexMap<String, String> {
types
.iter()
.map(AsRef::as_ref)
.map(split_type_and_doc)
.collect()
}
fn split_type_and_doc(type_and_doc: &str) -> (String, String) {
let mut split = type_and_doc.splitn(2, ' ');
let ty = split.next().unwrap_or_default().to_owned();
let doc = split.next().unwrap_or_default().trim().to_owned();
(ty, doc)
}