rep-grep 0.0.8

wgrep/write-grep CLI
use crate::patcher::Patcher;
use diffy_fork_filenames::{create_file_patch, PatchFormatter};
use std::{fs, fs::File, io::prelude::*, io::BufReader, path::PathBuf};

pub type Result<T, E = Error> = std::result::Result<T, E>;

pub(crate) struct Writer<'a> {
    path: PathBuf,
    patcher: &'a Patcher<'a>,
}

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("Invalid path: {0}")]
    InvalidPath(std::path::PathBuf),
    #[error(transparent)]
    File(#[from] std::io::Error),
    #[error("failed to move file: {0}")]
    TempfilePersist(#[from] tempfile::PersistError),
    #[error(transparent)]
    Patcher(#[from] crate::patcher::Error),
    #[error("invalid UTF-8 in replacement: {0}")]
    Utf8(#[from] std::str::Utf8Error),
}

impl<'a> Writer<'a> {
    pub(crate) fn new(path: PathBuf, patcher: &'a Patcher) -> Self {
        Self { path, patcher }
    }

    pub(crate) fn patch_preview(
        &self,
        color: bool,
        delete: bool,
    ) -> Result<String, crate::writer::Error> {
        let file = File::open(self.path.clone())?;
        let buf = BufReader::new(file);
        let lines: Vec<String> = buf
            .lines()
            .collect::<std::result::Result<_, _>>()?;
        let original = lines.join("\n");
        let modified = self.patcher.patch(lines, delete)?;
        let filename = match self.path.to_str() {
            Some(filename) => filename,
            None => return Err(Error::InvalidPath(self.path.clone())),
        };
        let original_filename = format!("a/{}", filename);
        let modified_filename = format!("b/{}", filename);
        let patch = create_file_patch(
            &original,
            &modified,
            original_filename.as_str(),
            modified_filename.as_str(),
        );
        let f = match color {
            true => PatchFormatter::new().with_color(),
            false => PatchFormatter::new(),
        };
        let result = f.fmt_patch(&patch).to_string();
        Ok(result)
    }

    pub(crate) fn write_file(&self, delete: bool) -> Result<()> {
        use memmap2::{Mmap, MmapMut};
        use std::ops::DerefMut;

        let source = File::open(self.path.clone())?;
        let meta = fs::metadata(self.path.clone())?;
        let mmap_source = unsafe { Mmap::map(&source)? };
        let lines = mmap_source
            .lines()
            .collect::<std::result::Result<Vec<_>, _>>()?;
        let mut replaced = self.patcher.patch(lines, delete)?;
        if mmap_source.ends_with("\n".as_bytes()) {
            replaced.push('\n');
        }

        let target = tempfile::NamedTempFile::new_in(
            self.path
                .parent()
                .ok_or_else(|| Error::InvalidPath(self.path.to_path_buf()))?,
        )?;
        let file = target.as_file();
        file.set_len(replaced.len() as u64)?;
        file.set_permissions(meta.permissions())?;

        if !replaced.is_empty() {
            let mut mmap_target = unsafe { MmapMut::map_mut(file)? };
            mmap_target.deref_mut().write_all(replaced.as_bytes())?;
            mmap_target.flush_async()?;
        }

        drop(mmap_source);
        drop(source);

        target.persist(fs::canonicalize(self.path.clone())?)?;
        Ok(())
    }
}