#![cfg_attr(feature = "fatal-warnings", deny(warnings))]
#![deny(clippy::correctness)]
#![warn(clippy::pedantic)]
#![allow(clippy::match_bool)]
#![allow(clippy::if_not_else)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::similar_names)]
#![allow(clippy::use_self)]
#![allow(clippy::single_match_else)]
#![allow(clippy::inline_always)]
#![allow(clippy::partialeq_ne_impl)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::non_ascii_literal)]
#![allow(clippy::enum_variant_names)]
#![allow(clippy::new_without_default)]
#![allow(clippy::struct_excessive_bools)]
use crate::options::{EntrypointOpt, LineTerminatorOpt};
use cargo_rdme::transform::IntralinkError;
use cargo_rdme::{
extract_doc_from_source_file, infer_line_terminator, inject_doc_in_readme, LineTerminator,
Project,
};
use cargo_rdme::{Doc, ProjectError, Readme};
use std::cell::Cell;
use std::path::{Path, PathBuf};
use thiserror::Error;
#[macro_use]
mod console;
mod options;
enum ExitCode {
Ok = 0,
Error = 1,
ReadmeNotUpdatedUncommittedChanges = 2,
CheckMismatch = 3,
CheckHasWarnings = 4,
}
impl From<RunError> for ExitCode {
fn from(value: RunError) -> ExitCode {
match value {
RunError::ProjectError(_)
| RunError::ExtractDocError(_)
| RunError::ReadmeError(_)
| RunError::NoEntrySourceFile
| RunError::NoReadmeFile
| RunError::NoRustdoc
| RunError::InjectDocError(_)
| RunError::TransformIntraLinkError(_)
| RunError::IOError(_) => ExitCode::Error,
RunError::ReadmeNotUpdatedUncommittedChanges => {
ExitCode::ReadmeNotUpdatedUncommittedChanges
}
RunError::CheckReadmeMismatch => ExitCode::CheckMismatch,
RunError::CheckHasWarnings => ExitCode::CheckHasWarnings,
}
}
}
#[derive(Error, Debug)]
enum RunError {
#[error("failed to get project info: {0}")]
ProjectError(ProjectError),
#[error("failed to extract rust doc: {0}")]
ExtractDocError(cargo_rdme::ExtractDocError),
#[error("failed to process README: {0}")]
ReadmeError(cargo_rdme::ReadmeError),
#[error("failed get crate's entry source file")]
NoEntrySourceFile,
#[error("crate's README file not found")]
NoReadmeFile,
#[error("crate-level rustdoc not found")]
NoRustdoc,
#[error("failed to inject the documentation in the README: {0}")]
InjectDocError(cargo_rdme::InjectDocError),
#[error("IO error: {0}")]
IOError(std::io::Error),
#[error(
"not updating README: it has uncommitted changes (use `--force` to bypass this check)"
)]
ReadmeNotUpdatedUncommittedChanges,
#[error("failed to transform intralinks: {0}")]
TransformIntraLinkError(IntralinkError),
#[error("README is not up to date")]
CheckReadmeMismatch,
#[error("README is up to date, but warnings were emitted")]
CheckHasWarnings,
}
impl From<ProjectError> for RunError {
fn from(e: ProjectError) -> RunError {
RunError::ProjectError(e)
}
}
impl From<cargo_rdme::ExtractDocError> for RunError {
fn from(e: cargo_rdme::ExtractDocError) -> RunError {
RunError::ExtractDocError(e)
}
}
impl From<cargo_rdme::ReadmeError> for RunError {
fn from(e: cargo_rdme::ReadmeError) -> RunError {
RunError::ReadmeError(e)
}
}
impl From<cargo_rdme::InjectDocError> for RunError {
fn from(e: cargo_rdme::InjectDocError) -> RunError {
RunError::InjectDocError(e)
}
}
impl From<std::io::Error> for RunError {
fn from(e: std::io::Error) -> RunError {
RunError::IOError(e)
}
}
impl From<IntralinkError> for RunError {
fn from(e: IntralinkError) -> RunError {
RunError::TransformIntraLinkError(e)
}
}
impl From<std::convert::Infallible> for RunError {
fn from(_: std::convert::Infallible) -> RunError {
unreachable!()
}
}
fn is_readme_up_to_date(
readme_path: impl AsRef<Path>,
new_readme: &Readme,
line_terminator: LineTerminator,
) -> Result<bool, RunError> {
let current_readme_raw: String = std::fs::read_to_string(readme_path)?;
let new_readme_raw: Vec<u8> = {
let mut bytes: Vec<u8> = Vec::with_capacity(32 * 1024);
new_readme.write(&mut bytes, line_terminator)?;
bytes
};
Ok(current_readme_raw.as_bytes() == new_readme_raw.as_slice())
}
fn entrypoint<'a>(project: &'a Project, entrypoint_opt: &EntrypointOpt) -> Option<&'a Path> {
match entrypoint_opt {
EntrypointOpt::Auto => {
project.get_lib_entryfile_path().or_else(|| project.get_bin_default_entryfile_path())
}
EntrypointOpt::Lib => project.get_lib_entryfile_path(),
EntrypointOpt::BinDefault => project.get_bin_default_entryfile_path(),
EntrypointOpt::BinName(name) => project.get_bin_entryfile_path(name),
}
}
fn line_terminator(
line_terminator_opt: LineTerminatorOpt,
readme_path: impl AsRef<Path>,
) -> std::io::Result<LineTerminator> {
match line_terminator_opt {
LineTerminatorOpt::Auto => infer_line_terminator(readme_path),
LineTerminatorOpt::Lf => Ok(LineTerminator::Lf),
LineTerminatorOpt::CrLf => Ok(LineTerminator::CrLf),
}
}
struct Warnings {
had_warnings: bool,
}
fn transform_doc(
doc: &Doc,
project: &Project,
entrypoint: impl AsRef<Path>,
options: &options::Options,
) -> Result<(Doc, Warnings), RunError> {
use cargo_rdme::transform::{
DocTransform, DocTransformIntralinks, DocTransformRustMarkdownTag,
DocTransformRustRemoveComments,
};
let transform = DocTransformRustRemoveComments::new();
let doc = transform.transform(doc)?;
let transform = DocTransformRustMarkdownTag::new();
let doc = transform.transform(&doc)?;
let had_warnings = Cell::new(false);
let transform = DocTransformIntralinks::new(
project.get_package_name(),
entrypoint,
|msg| {
print_warning!("{}", msg);
had_warnings.set(true);
},
options.intralinks.clone(),
);
Ok((transform.transform(&doc)?, Warnings { had_warnings: had_warnings.into_inner() }))
}
fn git_is_current(path: impl AsRef<Path>) -> Option<bool> {
use git2::{Repository, Status};
let repository = Repository::discover(path.as_ref().parent()?).ok()?;
let repository_path = repository.path().parent()?;
let path_repository_base = path.as_ref().strip_prefix(repository_path).ok()?;
let status = repository.status_file(path_repository_base).ok()?;
Some(status == Status::CURRENT)
}
fn update_readme(
new_readme: &Readme,
readme_path: impl AsRef<Path>,
line_terminator: LineTerminator,
ignore_uncommitted_changes: bool,
) -> Result<(), RunError> {
match ignore_uncommitted_changes || git_is_current(&readme_path).unwrap_or(true) {
true => Ok(new_readme.write_to_file(&readme_path, line_terminator)?),
false => Err(RunError::ReadmeNotUpdatedUncommittedChanges),
}
}
fn run(options: options::Options) -> Result<(), RunError> {
let project: Project = match options.workspace_project {
None => Project::from_current_dir()?,
Some(ref project) => Project::from_current_dir_workspace_project(project)?,
};
let entryfile: &Path =
entrypoint(&project, &options.entrypoint).ok_or(RunError::NoEntrySourceFile)?;
let doc: Doc = match extract_doc_from_source_file(entryfile)? {
None => return Err(RunError::NoRustdoc),
Some(doc) => doc,
};
let (doc, warnings) = transform_doc(&doc, &project, entryfile, &options)?;
let readme_path: PathBuf = match options.readme_path {
None => project.get_readme_path().ok_or(RunError::NoReadmeFile)?,
Some(path) => {
if !path.is_file() {
return Err(RunError::NoReadmeFile);
}
path
}
};
let original_readme: Readme = Readme::from_file(&readme_path)?;
let new_readme = inject_doc_in_readme(&original_readme, &doc)?;
if !new_readme.had_marker {
let msg = indoc::formatdoc! { "
No marker found in the README file ({readme_filepath}).
cargo-rdme expects a marker in the README where the crate’s documentation will
be inserted. This is the marker you should add to your README:
{marker}",
readme_filepath = readme_path.display(),
marker = cargo_rdme::MARKER_RDME,
};
print_info!("{}", msg);
}
let line_terminator = line_terminator(options.line_terminator, &readme_path)?;
match options.check {
false => update_readme(&new_readme.readme, readme_path, line_terminator, options.force),
true => {
if !is_readme_up_to_date(&readme_path, &new_readme.readme, line_terminator)? {
return Err(RunError::CheckReadmeMismatch);
}
if warnings.had_warnings && !options.no_fail_on_warnings {
return Err(RunError::CheckHasWarnings);
}
Ok(())
}
}
}
fn main() {
let cmd_options = options::cmd_options();
let exit_code: ExitCode = match std::env::current_dir() {
Ok(current_dir) => match options::config_file_options(current_dir) {
Ok(config_file_options) => {
let options = options::merge_options(cmd_options, config_file_options);
match run(options) {
Ok(_) => ExitCode::Ok,
Err(e) => {
print_error!("{}", &e);
e.into()
}
}
}
Err(e) => {
print_error!("unable to read config file: {}", e);
ExitCode::Error
}
},
Err(e) => {
print_error!("unable to get current directory: {}", e);
ExitCode::Error
}
};
std::process::exit(exit_code as i32);
}