2022-10-20 23:17:42 +02:00
use clap ::{ Arg , Command } ;
2024-12-22 13:00:52 +01:00
use console ::style ;
2022-10-20 23:17:42 +02:00
use eyre ::{ Context , ContextCompat } ;
use std ::io ::prelude ::* ;
use std ::{ env ::current_dir , io ::Read , path ::PathBuf } ;
2022-10-20 23:43:33 +02:00
use tracing_subscriber ::layer ::SubscriberExt ;
use tracing_subscriber ::util ::SubscriberInitExt ;
2022-10-20 23:17:42 +02:00
2022-10-21 00:45:57 +02:00
pub fn main ( ) -> eyre ::Result < ( ) > {
2022-10-20 23:17:42 +02:00
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 ) ,
2022-10-20 23:43:33 +02:00
) . 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 [fatal] " ) )
2022-10-20 23:17:42 +02:00
. get_matches ( ) ;
2022-10-20 23:43:33 +02:00
let log_level = match matches . get_one ::< String > ( " log-level " ) . map ( | f | f . as_str ( ) ) {
Some ( " off " ) = > " off " ,
Some ( " info " ) = > " info " ,
Some ( " debug " ) = > " debug " ,
Some ( " warn " ) = > " warn " ,
Some ( " error " ) = > " error " ,
_ = > " error " ,
} ;
tracing_subscriber ::registry ( )
. with ( tracing_subscriber ::EnvFilter ::new ( format! (
" gitignore={} " ,
log_level
) ) )
. with ( tracing_subscriber ::fmt ::layer ( ) )
. init ( ) ;
let term = console ::Term ::stdout ( ) ;
2022-10-20 23:17:42 +02:00
let pattern = matches
. get_one ::< String > ( " pattern " )
. context ( " missing [pattern] " ) ? ;
2022-10-20 23:43:33 +02:00
add_gitignore_pattern ( term , pattern )
2022-10-20 23:17:42 +02:00
}
enum GitActions {
2022-10-21 00:23:12 +02:00
AddPattern { gitignore_path : PathBuf } ,
CreateIgnoreAndAddPattern { git_path : PathBuf } ,
2022-10-20 23:17:42 +02:00
}
2022-10-20 23:43:33 +02:00
fn add_gitignore_pattern ( term : console ::Term , pattern : & String ) -> eyre ::Result < ( ) > {
2024-12-22 13:00:52 +01:00
println! ( " git ignore: Add pattern " ) ;
2022-10-20 23:17:42 +02:00
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 ) ? {
2022-10-21 00:23:12 +02:00
GitSearchResult ::Git ( _gitpath ) = > GitActions ::AddPattern {
2022-10-20 23:17:42 +02:00
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 {
2022-10-21 00:23:12 +02:00
GitActions ::AddPattern { gitignore_path } = > {
2024-12-22 13:00:52 +01:00
println! ( " Found existing {} " , style ( " .gitignore " ) . green ( ) ) ;
2022-10-20 23:17:42 +02:00
let mut gitignore_file = open_gitignore_file ( & gitignore_path ) ? ;
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 ) {
2024-12-22 13:00:52 +01:00
println! (
" .gitignore already contains pattern, {} " ,
style ( " skipping... " ) . blue ( )
) ;
2022-10-20 23:17:42 +02:00
return Ok ( ( ) ) ;
}
2024-12-22 13:00:52 +01:00
println! ( " adding pattern to file " ) ;
2022-10-20 23:17:42 +02:00
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 } = > {
2024-12-22 13:00:52 +01:00
println! (
" could not find {} file, creating one in the root of the git repository " ,
style ( " .gitignore " ) . yellow ( )
) ;
2024-12-22 11:33:14 +01:00
let mut gitignore_file = create_gitignore_file ( git_path ) ? ;
2024-12-22 13:00:52 +01:00
println! ( " adding pattern to file " ) ;
2022-10-20 23:17:42 +02:00
writeln! ( gitignore_file , " {} " , pattern ) . context ( " could not write contents to file " ) ? ;
gitignore_file
. sync_all ( )
. context ( " failed to write data to disk " ) ? ;
}
}
2022-10-21 00:07:44 +02:00
// TODO: Run git rm -r --cached --pathspec <pattern> on the .git root
let output = std ::process ::Command ::new ( " git " )
2022-10-21 00:11:12 +02:00
. arg ( " rm " )
2022-10-21 00:07:44 +02:00
. arg ( " -r " )
. arg ( " --cached " )
2024-12-22 11:33:14 +01:00
. arg ( " -f " )
2022-10-21 00:11:12 +02:00
. arg ( " --ignore-unmatch " )
2022-10-21 00:07:44 +02:00
. arg ( pattern )
. output ( )
. context ( " could not process git remove from index command " ) ? ;
String ::from_utf8 ( output . stdout ) ?
. lines ( )
. chain ( String ::from_utf8 ( output . stderr ) ? . lines ( ) )
2024-12-22 13:00:52 +01:00
. map ( | l | {
// make rm 'path' look nice
if l . contains ( " rm " ) {
if let Some ( ( _ , pruned_first ) ) = l . split_once ( " ' " ) {
if let Some ( ( content , _ ) ) = pruned_first . rsplit_once ( " ' " ) {
return content ;
}
}
}
l
} )
. for_each ( | l | println! ( " removed from git history: {} " , style ( l ) . yellow ( ) ) ) ;
2022-10-21 00:07:44 +02:00
if ! output . status . success ( ) {
return Err ( eyre ::anyhow! ( " failed to run git index command " ) ) ;
}
2022-10-20 23:17:42 +02:00
2022-10-21 00:11:12 +02:00
term . write_line ( " git successfully removed files " ) ? ;
2022-10-20 23:17:42 +02:00
Ok ( ( ) )
}
2024-12-22 11:33:14 +01:00
fn create_gitignore_file ( gitroot : PathBuf ) -> eyre ::Result < std ::fs ::File > {
2022-10-20 23:17:42 +02:00
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 ( )
) ) ? ;
2024-12-22 11:33:14 +01:00
Ok ( file )
2022-10-20 23:17:42 +02:00
}
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 ( ) ;
2024-12-22 11:33:14 +01:00
if file_name . to_str ( ) . context ( " could not convert to str " ) ? = = " .git " {
return Ok ( GitSearchResult ::Git ( entry . path ( ) ) ) ;
2022-10-20 23:17:42 +02:00
}
}
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 ( ) ;
2024-12-22 11:33:14 +01:00
if file_name . to_str ( ) . context ( " could not convert to str " ) ? = = " .gitignore " {
return Ok ( GitSearchResult ::GitIgnore ( entry . path ( ) ) ) ;
}
}
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 ( ) ;
if file_name . to_str ( ) . context ( " could not convert to str " ) ? = = " .git " {
return Ok ( GitSearchResult ::Git ( entry . path ( ) ) ) ;
2022-10-20 23:17:42 +02:00
}
}
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 )
2022-10-20 21:48:47 +02:00
}