All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
350 lines
11 KiB
Rust
350 lines
11 KiB
Rust
use clap::{Arg, Command};
|
|
use console::style;
|
|
use eyre::{Context, ContextCompat, OptionExt};
|
|
use std::io::prelude::*;
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use std::{env::current_dir, io::Read, path::PathBuf};
|
|
use tracing_subscriber::layer::SubscriberExt;
|
|
use tracing_subscriber::util::SubscriberInitExt;
|
|
|
|
const ZSH_FILE_CONTENTS: &[u8] = b"#!/usr/bin/env zsh
|
|
set -e
|
|
|
|
kignore $@
|
|
";
|
|
|
|
const SH_FILE_CONTENTS: &[u8] = b"#!/usr/bin/env sh
|
|
set -e
|
|
|
|
kignore $@
|
|
";
|
|
|
|
const BASH_FILE_CONTENTS: &[u8] = b"#!/usr/bin/env bash
|
|
set -e
|
|
|
|
kignore $@
|
|
";
|
|
|
|
pub 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"),
|
|
)
|
|
.arg(
|
|
Arg::new("log-level")
|
|
.long("log-level")
|
|
.default_value("warn")
|
|
.help("choose a log level and get more messages")
|
|
.long_help("Choose a log level and get more message, defaults to [warn]"),
|
|
)
|
|
.subcommand(
|
|
clap::Command::new("init")
|
|
.subcommand_required(true)
|
|
.subcommand(Command::new("zsh"))
|
|
.subcommand(Command::new("sh"))
|
|
.subcommand(Command::new("bash")),
|
|
)
|
|
.get_matches();
|
|
|
|
match matches.subcommand() {
|
|
Some(("init", args)) => match args
|
|
.subcommand()
|
|
.expect("should never be able to call on init")
|
|
{
|
|
("zsh", _) => init_script(ShellType::Zsh),
|
|
("bash", _) => init_script(ShellType::Bash),
|
|
("sh", _) => init_script(ShellType::Shell),
|
|
(subcommand, _) => {
|
|
panic!("cannot call on subcommand: {}", subcommand);
|
|
}
|
|
},
|
|
_ => {
|
|
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();
|
|
|
|
let pattern = matches
|
|
.get_one::<String>("pattern")
|
|
.context("missing [pattern]")?;
|
|
|
|
add_gitignore_pattern(term, pattern)
|
|
}
|
|
}
|
|
}
|
|
|
|
enum GitActions {
|
|
AddPattern { gitignore_path: PathBuf },
|
|
CreateIgnoreAndAddPattern { git_path: PathBuf },
|
|
}
|
|
|
|
fn add_gitignore_pattern(term: console::Term, pattern: &String) -> eyre::Result<()> {
|
|
println!("git ignore: Add pattern");
|
|
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 {
|
|
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 { gitignore_path } => {
|
|
println!("Found existing {}", style(".gitignore").green());
|
|
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) {
|
|
println!(
|
|
".gitignore already contains pattern, {}",
|
|
style("skipping...").blue()
|
|
);
|
|
return Ok(());
|
|
}
|
|
|
|
println!("adding pattern to file");
|
|
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 } => {
|
|
println!(
|
|
"could not find {} file, creating one in the root of the git repository",
|
|
style(".gitignore").yellow()
|
|
);
|
|
let mut gitignore_file = create_gitignore_file(git_path)?;
|
|
println!("adding pattern to file");
|
|
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 --pathspec <pattern> on the .git root
|
|
let output = std::process::Command::new("git")
|
|
.arg("rm")
|
|
.arg("-r")
|
|
.arg("--cached")
|
|
.arg("-f")
|
|
.arg("--ignore-unmatch")
|
|
.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())
|
|
.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()));
|
|
|
|
if !output.status.success() {
|
|
return Err(eyre::anyhow!("failed to run git index command"));
|
|
}
|
|
|
|
term.write_line("git successfully removed files")?;
|
|
|
|
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()
|
|
))?;
|
|
|
|
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();
|
|
if 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();
|
|
|
|
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()));
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
enum ShellType {
|
|
Bash,
|
|
Shell,
|
|
Zsh,
|
|
}
|
|
|
|
fn init_script(shell: ShellType) -> eyre::Result<()> {
|
|
let bin_dir = dirs::executable_dir().ok_or_eyre("failed to find executable dir")?;
|
|
|
|
let script = match shell {
|
|
ShellType::Bash => BASH_FILE_CONTENTS,
|
|
ShellType::Shell => SH_FILE_CONTENTS,
|
|
ShellType::Zsh => ZSH_FILE_CONTENTS,
|
|
};
|
|
|
|
let alias_script = bin_dir.join("git-ignore");
|
|
if let Ok(existing_file) = std::fs::read(&alias_script) {
|
|
if existing_file == script {
|
|
return Ok(());
|
|
}
|
|
} else {
|
|
std::fs::create_dir_all(&bin_dir).context("failed to create bin dir")?;
|
|
}
|
|
|
|
let mut file = std::fs::OpenOptions::new()
|
|
.write(true)
|
|
.create(true)
|
|
.truncate(true)
|
|
.open(&alias_script)?;
|
|
|
|
file.write_all(script)?;
|
|
file.flush()?;
|
|
|
|
// Set the file to be executable
|
|
let metadata = file.metadata()?;
|
|
let mut permissions = metadata.permissions();
|
|
permissions.set_mode(0o755); // rwxr-xr-x
|
|
file.set_permissions(permissions)?;
|
|
|
|
println!(
|
|
"successfully wrote alias to {}",
|
|
style(alias_script.display()).green()
|
|
);
|
|
|
|
Ok(())
|
|
}
|