diff --git a/crates/gitignore/Cargo.toml b/crates/gitignore/Cargo.toml index e1c5c51..56886ba 100644 --- a/crates/gitignore/Cargo.toml +++ b/crates/gitignore/Cargo.toml @@ -6,3 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +clap = { version = "4.0.17", features = ["env", "unicode", "string"] } +eyre = "0.6.8" diff --git a/crates/gitignore/src/main.rs b/crates/gitignore/src/main.rs index e7a11a9..9581027 100644 --- a/crates/gitignore/src/main.rs +++ b/crates/gitignore/src/main.rs @@ -1,3 +1,190 @@ -fn main() { - println!("Hello, world!"); +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), + ) + .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) }