use std::{
io::{self, BufWriter, Read, Write},
process::ExitCode,
};
use anyhow::Context;
use camino::{Utf8Path, Utf8PathBuf};
use cap_std::{ambient_authority, fs_utf8::Dir};
use mdtablefix::{
io::SourceDocument,
report::{FileReport, render_summary},
};
use rayon::prelude::*;
use tracing::debug;
mod command;
mod driver;
mod git_inputs;
mod metrics;
mod select;
use command::{Cli, FormatOpts, format_lines, formatting_closure};
use driver::{ExitStatus, Formatter, Inputs, Mode, analyse, exit_status, in_argument_order};
use metrics::{record_analysis, record_run};
use select::ConflictGuard;
fn open_file_parent(path: &Utf8Path) -> anyhow::Result<(Dir, Utf8PathBuf)> {
let parent = path
.parent()
.filter(|parent| !parent.as_str().is_empty())
.unwrap_or(Utf8Path::new("."));
let file_name = path
.file_name()
.with_context(|| format!("selecting file name from {path}"))?;
let directory = Dir::open_ambient_dir(parent, ambient_authority())
.with_context(|| format!("opening {parent}"))?;
Ok((directory, Utf8PathBuf::from(file_name)))
}
fn render_stdin_output(document: &SourceDocument<'_>, fixed: &[String]) -> String {
if fixed.is_empty() {
document.ending().as_str().to_string()
} else {
document.render(fixed)
}
}
fn format_stdin(input: &str, opts: FormatOpts) -> String {
let document = SourceDocument::parse(input);
driver::report_line_endings(document.counts(), "stdin", None);
render_stdin_output(&document, &format_lines(document.body(), opts))
}
fn analyse_one(
mode: Mode,
guard: &ConflictGuard,
path: &Utf8Path,
format: &Formatter,
) -> anyhow::Result<(FileReport, String)> {
open_file_parent(path)
.and_then(|(directory, storage_key)| {
analyse(mode, guard, &directory, path, &storage_key, format)
})
.with_context(|| format!("{} {}", mode.verb(), path))
}
fn write_stdout(text: &str) -> anyhow::Result<()> {
let mut stdout = io::stdout().lock();
stdout.write_all(text.as_bytes())?;
stdout.flush()?;
Ok(())
}
fn is_broken_pipe(error: &anyhow::Error) -> bool {
error.chain().any(|cause| {
cause
.downcast_ref::<io::Error>()
.is_some_and(|error| error.kind() == io::ErrorKind::BrokenPipe)
})
}
fn failed_run(mode: Mode) -> ExitStatus {
let status = exit_status(mode, false, true);
record_run(mode, status);
status
}
fn run() -> anyhow::Result<ExitStatus> {
let cli = Cli::parse_validated();
let mode = cli.mode();
metrics::describe_metrics();
let (inputs, guard) = if cli.selects_from_git() {
let working_directory = match git_inputs::working_directory() {
Ok(working_directory) => working_directory,
Err(error) => {
eprintln!("{error:?}");
return Ok(failed_run(mode));
}
};
match git_inputs::resolve(&cli, mode, &working_directory) {
Ok(selection) => {
if let Some(warning) = selection.skipped_warning() {
eprintln!("{warning}");
}
report_selection(&selection);
(selection.inputs, selection.guard)
}
Err(error) => {
eprintln!("mdtablefix: {}", error.diagnostic());
return Ok(failed_run(mode));
}
}
} else {
match Inputs::resolve(cli.files) {
Ok(inputs) => (inputs, ConflictGuard::unguarded()),
Err(error) => {
eprintln!("{error:?}");
return Ok(failed_run(mode));
}
}
};
let result = match inputs {
Inputs::Stdin => run_stdin(cli.opts),
Inputs::Files(files) => run_files(mode, &guard, &files, cli.opts),
};
let result = match result {
Err(error) if is_broken_pipe(&error) => Ok(ExitStatus::Success),
settled => settled,
};
record_run(mode, result.as_ref().copied().unwrap_or(ExitStatus::Error));
result
}
fn report_selection(selection: &git_inputs::GitSelection) {
let statistics = selection.selection;
debug!(
candidates = statistics.candidates,
selected = statistics.selected,
extension_count = statistics.extension_count,
"selected files from the repository"
);
}
fn run_stdin(opts: FormatOpts) -> anyhow::Result<ExitStatus> {
let mut input = String::new();
io::stdin().read_to_string(&mut input)?;
write_stdout(&format_stdin(&input, opts))?;
Ok(ExitStatus::Success)
}
const ANALYSIS_CHUNK: usize = 256;
fn run_files(
mode: Mode,
guard: &ConflictGuard,
files: &[Utf8PathBuf],
opts: FormatOpts,
) -> anyhow::Result<ExitStatus> {
let format = formatting_closure(opts);
let mut changed = 0;
let mut unchanged = 0;
let mut errored = 0;
let mut stdout = BufWriter::new(io::stdout().lock());
for chunk in files.chunks(ANALYSIS_CHUNK) {
let results = match mode {
Mode::InPlace => chunk
.iter()
.map(|path| record_analysis(mode, path, || analyse_one(mode, guard, path, &format)))
.collect(),
Mode::Print | Mode::Check | Mode::Diff | Mode::ListFiles => in_argument_order(
chunk
.par_iter()
.enumerate()
.map(|(index, path)| {
(
index,
record_analysis(mode, path, || analyse_one(mode, guard, path, &format)),
)
})
.collect(),
),
};
for result in results {
match result {
Ok((report, payload)) => {
if report.is_changed {
changed += 1;
} else {
unchanged += 1;
}
stdout.write_all(payload.as_bytes())?;
}
Err(error) => {
eprintln!("{error:?}");
errored += 1;
}
}
}
}
stdout.flush()?;
if mode.reports() {
eprintln!("{}", render_summary(changed, unchanged, errored));
}
Ok(exit_status(mode, changed > 0, errored > 0))
}
fn main() -> ExitCode {
match run() {
Ok(status) => status.code(),
Err(error) => {
eprintln!("{error:?}");
ExitStatus::Error.code()
}
}
}
#[cfg(test)]
#[path = "main_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "main_git_selection_tests.rs"]
mod git_selection_tests;