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 ") .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 ` 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::("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 { 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 { 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 { 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 { 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) }