gitignore_inner 0.1.0

gitignore_inner is the internal module for cargo
Documentation
use clap::{Arg, Command};
use eyre::{Context, ContextCompat};
use std::io::prelude::*;
use std::{env::current_dir, io::Read, path::PathBuf};

fn main() -> eyre::Result<()> {
    let matches = Command::new("gitignore")
        .version("0.1")
        .author("Kasper J. Hermansen <contact@kjuulh.io>")
        .about("Easily ignore items and remove from git state")
        .long_about("git ignore is a utility tool for easily adding patterns to your .gitignore file.
Easily add patterns using `git ignore <pattern>` this will by default also help you remove committed code violating these patterns
                    ")
        .propagate_version(true)
        .arg(
            Arg::new("pattern")
                .help("the pattern you want to ignore")
                .long_help("the pattern you want to ignore in the nearest .gitignore file")
                .required(true),
        ).arg(
            Arg::new("log-level").long("log-level").help("choose a log level and get more messages").long_help("Choose a log level and get more message, defaults to [INFO]"))
        .get_matches();

    let pattern = matches
        .get_one::<String>("pattern")
        .context("missing [pattern]")?;

    add_gitignore_pattern(pattern)
}

enum GitActions {
    AddPattern {
        git_path: PathBuf,
        gitignore_path: PathBuf,
    },
    CreateIgnoreAndAddPattern {
        git_path: PathBuf,
    },
}

fn add_gitignore_pattern(pattern: &String) -> eyre::Result<()> {
    let curdir = current_dir().context(
        "could not find current_dir, you may not have permission to access that directory",
    )?;
    let actions = match search_for_dotgitignore(&curdir)? {
        // If we have an ignore path, make sure it is in a git repo as well
        GitSearchResult::GitIgnore(ignorepath) => match search_for_git_root(&curdir)? {
            GitSearchResult::Git(gitpath) => GitActions::AddPattern {
                git_path: gitpath,
                gitignore_path: ignorepath,
            },
            _ => return Err(eyre::anyhow!("could not find parent git directory")),
        },
        // Find the nearest git repo
        GitSearchResult::Git(gitpath) => {
            GitActions::CreateIgnoreAndAddPattern { git_path: gitpath }
        } // We will always have either above, or an error so we have no default arm
    };

    match actions {
        GitActions::AddPattern {
            git_path,
            gitignore_path,
        } => {
            let mut gitignore_file = open_gitignore_file(&gitignore_path)?;
            // TODO: search for pattern in file
            let mut gitignore_content = String::new();
            gitignore_file
                .read_to_string(&mut gitignore_content)
                .context(format!(
                    "could not read file: {}",
                    gitignore_path.to_string_lossy()
                ))?;
            if gitignore_content.contains(pattern) {
                return Ok(());
            }

            writeln!(gitignore_file, "{}", pattern).context("could not write contents to file")?;
            gitignore_file
                .sync_all()
                .context("failed to write data to disk")?;
        }
        GitActions::CreateIgnoreAndAddPattern { git_path } => {
            // TODO: Create gitignore file in root
            let mut gitignore_file = create_gitignore_file(&git_path)?;
            // TODO: do same as above
            writeln!(gitignore_file, "{}", pattern).context("could not write contents to file")?;
            gitignore_file
                .sync_all()
                .context("failed to write data to disk")?;
        }
    }

    // TODO: Run git rm -r --cached on the .git root

    Ok(())
}

fn create_gitignore_file(gitroot: &PathBuf) -> eyre::Result<std::fs::File> {
    let mut ignore_path = gitroot.clone();
    if !ignore_path.pop() {
        return Err(eyre::anyhow!("could not open parent dir"));
    }
    ignore_path.push(".gitignore");
    let file = std::fs::File::create(ignore_path.clone()).context(format!(
        "could not create file at path: {}",
        ignore_path.to_string_lossy()
    ))?;

    Ok(file)
}

fn open_gitignore_file(gitignore: &PathBuf) -> eyre::Result<std::fs::File> {
    let file = std::fs::OpenOptions::new()
        .read(true)
        .write(true)
        .open(gitignore)
        .context(format!(
            "could not create file at path: {}",
            gitignore.to_string_lossy()
        ))?;

    return Ok(file);
}

enum GitSearchResult {
    GitIgnore(PathBuf),
    Git(PathBuf),
}

fn search_for_git_root(path: &PathBuf) -> eyre::Result<GitSearchResult> {
    if !path.is_dir() {
        return Err(eyre::anyhow!(
            "path is not a dir: {}",
            path.to_string_lossy()
        ));
    }

    let direntries = std::fs::read_dir(path)
        .context(format!("could not open dir: {}", path.to_string_lossy()))?;
    for direntry in direntries {
        let entry = direntry.context("could not access file")?;

        let file_name = entry.file_name().to_os_string();
        match file_name.to_str().context("could not convert to str")? {
            ".git" => return Ok(GitSearchResult::Git(entry.path())),
            _ => {}
        }
    }

    let mut upwards_par = path.clone();
    if !upwards_par.pop() {
        return Err(eyre::anyhow!(
            "no parent exists, cannot check further, you may not be in a git repository"
        ));
    }

    search_for_git_root(&upwards_par)
}

fn search_for_dotgitignore(path: &PathBuf) -> eyre::Result<GitSearchResult> {
    if !path.is_dir() {
        return Err(eyre::anyhow!(
            "path is not a dir: {}",
            path.to_string_lossy()
        ));
    }

    let direntries = std::fs::read_dir(path)
        .context(format!("could not open dir: {}", path.to_string_lossy()))?;
    for direntry in direntries {
        let entry = direntry.context("could not access file")?;

        let file_name = entry.file_name().to_os_string();

        match file_name.to_str().context("could not convert to str")? {
            ".gitignore" => return Ok(GitSearchResult::GitIgnore(entry.path())),
            ".git" => return Ok(GitSearchResult::Git(entry.path())),
            _ => {}
        }
    }

    let mut upwards_par = path.clone();
    if !upwards_par.pop() {
        return Err(eyre::anyhow!(
            "no parent exists, cannot check further, you may not be in a git repository"
        ));
    }

    search_for_dotgitignore(&upwards_par)
}