use crate::options::{EntrypointOpt, LineTerminatorOpt};
use cargo_rdme::transform::IntralinkError;
use cargo_rdme::{Doc, ProjectError, Readme};
use cargo_rdme::{
LineTerminator, PackageTarget, Project, extract_doc_from_source_file, infer_line_terminator,
inject_doc_in_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::NoTargetPackage
| 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 to get crate's entry source file")]
NoEntrySourceFile,
#[error("failed to get crate's target package")]
NoTargetPackage,
#[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 package_target(project: &Project, entrypoint_opt: &EntrypointOpt) -> Option<PackageTarget> {
let bin_default = || {
project
.get_bin_default_crate_name()
.map(|name| PackageTarget::Bin { crate_name: name.to_owned() })
};
match entrypoint_opt {
EntrypointOpt::Auto => {
project.get_lib_entryfile_path().map(|_| PackageTarget::Lib).or_else(bin_default)
}
EntrypointOpt::Lib => Some(PackageTarget::Lib),
EntrypointOpt::BinDefault => bin_default(),
EntrypointOpt::BinName(name) => Some(PackageTarget::Bin { crate_name: name.clone() }),
}
}
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,
package_target: PackageTarget,
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().as_str().to_owned(),
package_target,
options.workspace_project.clone(),
project.get_manifest_path().clone(),
|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 gix::bstr::BString;
let repo = gix::discover(path.as_ref().parent()?).ok()?;
let work_dir = repo.workdir()?;
let path_in_repo = path.as_ref().strip_prefix(work_dir).ok()?;
let path_bstr = gix::bstr::BStr::new(path_in_repo.as_os_str().as_encoded_bytes());
let index = repo.index().ok()?;
if index.entry_by_path(path_bstr).is_none() {
return Some(false);
}
let cwd = std::env::current_dir().ok()?;
let path = path.as_ref().strip_prefix(&cwd).ok()?;
let pattern = BString::from(path.as_os_str().as_encoded_bytes());
let items: Result<Vec<_>, _> =
repo.status(gix::progress::Discard).ok()?.into_iter([pattern]).ok()?.collect();
Some(items.ok()?.is_empty())
}
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(options.manifest_path.as_deref())?,
Some(ref project) => {
Project::from_current_dir_workspace_project(options.manifest_path.as_deref(), project)?
}
};
let entryfile: &Path =
entrypoint(&project, &options.entrypoint).ok_or(RunError::NoEntrySourceFile)?;
let package_target: PackageTarget =
package_target(&project, &options.entrypoint).ok_or(RunError::NoTargetPackage)?;
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, package_target, &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, options.heading_base_level)?;
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 install_rust_toolchain_for_intralinks() -> Result<(), IntralinkError> {
use cargo_rdme::transform::{
EXPECTED_RUST_TOOLCHAIN, install_expected_rust_toolchain,
is_expected_rust_toolchain_installed,
};
if is_expected_rust_toolchain_installed()? {
print_info!("rust toolchain `{}` is already installed.", EXPECTED_RUST_TOOLCHAIN);
return Ok(());
}
print_info!("installing rust toolchain `{}`...", EXPECTED_RUST_TOOLCHAIN);
install_expected_rust_toolchain()?;
print_info!("rust toolchain `{}` installed.", EXPECTED_RUST_TOOLCHAIN);
Ok(())
}
fn main() {
let exit_code: ExitCode = match options::command() {
options::Command::InstallRustToolchainForIntralinks => {
match install_rust_toolchain_for_intralinks() {
Ok(()) => ExitCode::Ok,
Err(e) => {
print_error!("failed to install rust toolchain: {}", e);
ExitCode::Error
}
}
}
options::Command::Run(cmd_options) => {
let directory = cmd_options
.manifest_path()
.and_then(|path| path.parent())
.map_or_else(std::env::current_dir, |p| Ok(p.to_owned()));
match directory {
Ok(dir) => match options::config_file_options(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);
}