init: gig, the program that manages you gitignores
This commit is contained in:
commit
58328cce82
12 changed files with 1449 additions and 0 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
use flake
|
||||
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
### Rust ###
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug
|
||||
target
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# Generated by cargo mutants
|
||||
# Contains mutation testing data
|
||||
**/mutants.out*/
|
||||
|
||||
# RustRover
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
### End of Rust ###
|
||||
|
||||
### Nix ###
|
||||
# Ignore build outputs from performing a nix-build or `nix build` command
|
||||
result
|
||||
result-*
|
||||
|
||||
# Ignore automatically generated direnv output
|
||||
.direnv
|
||||
### End of Nix ###
|
||||
|
||||
1087
Cargo.lock
generated
Normal file
1087
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "gig"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.101"
|
||||
clap = { version = "4.5.57", features = ["derive"] }
|
||||
dialoguer = "0.12.0"
|
||||
directories = "6.0.0"
|
||||
figlet-rs = "0.1.5"
|
||||
git2 = "0.20.4"
|
||||
glob = "0.3.3"
|
||||
indicatif = "0.18.3"
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = "0.3.22"
|
||||
3
README.md
Normal file
3
README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# `gig` - the .gitignore cli
|
||||
|
||||
no docs yet, sorry
|
||||
77
flake.lock
generated
Normal file
77
flake.lock
generated
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1769996383,
|
||||
"narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "57928607ea566b5db3ad13af0e57e921e6b12381",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"import-tree": {
|
||||
"locked": {
|
||||
"lastModified": 1763762820,
|
||||
"narHash": "sha256-ZvYKbFib3AEwiNMLsejb/CWs/OL/srFQ8AogkebEPF0=",
|
||||
"owner": "vic",
|
||||
"repo": "import-tree",
|
||||
"rev": "3c23749d8013ec6daa1d7255057590e9ca726646",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "vic",
|
||||
"repo": "import-tree",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1770562336,
|
||||
"narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d6c71932130818840fc8fe9509cf50be8c64634f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"lastModified": 1769909678,
|
||||
"narHash": "sha256-cBEymOf4/o3FD5AZnzC3J9hLbiZ+QDT/KDuyHXVJOpM=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"rev": "72716169fe93074c333e8d0173151350670b824c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-parts": "flake-parts",
|
||||
"import-tree": "import-tree",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
13
flake.nix
Normal file
13
flake.nix
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
description = "A very basic flake";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||
import-tree.url = "github:vic/import-tree";
|
||||
};
|
||||
|
||||
outputs = inputs:
|
||||
inputs.flake-parts.lib.mkFlake {inherit inputs;}
|
||||
(inputs.import-tree ./packages/nix);
|
||||
}
|
||||
18
packages/nix/package.nix
Normal file
18
packages/nix/package.nix
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{...}: {
|
||||
perSystem = {pkgs, ...}: {
|
||||
packages.gig = pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "gig";
|
||||
version = "v0.0.1";
|
||||
|
||||
src = ../..;
|
||||
cargoLock.lockFile = ../../Cargo.lock;
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
openssl
|
||||
libgit2
|
||||
];
|
||||
|
||||
nativeBuildInputs = with pkgs; [pkg-config];
|
||||
};
|
||||
};
|
||||
}
|
||||
12
packages/nix/shell.nix
Normal file
12
packages/nix/shell.nix
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{...}: {
|
||||
perSystem = {pkgs, ...}: {
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
cargo
|
||||
pkg-config
|
||||
openssl
|
||||
libgit2
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
5
packages/nix/systems.nix
Normal file
5
packages/nix/systems.nix
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{...}: {
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
];
|
||||
}
|
||||
93
src/lib.rs
Normal file
93
src/lib.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
use clap::Parser;
|
||||
use git2::Repository;
|
||||
use glob::MatchOptions;
|
||||
use indicatif::ProgressBar;
|
||||
use tracing::{info, warn};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
use directories::ProjectDirs;
|
||||
use anyhow::{Result, Context};
|
||||
|
||||
const GITIGNORE_URL: &str = "https://git.teesh.dev/many/gitignore";
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct Args {
|
||||
#[arg(required = true, num_args = 1..)]
|
||||
pub templates: Vec<String>
|
||||
}
|
||||
|
||||
pub struct GitignoreRepo {
|
||||
path: PathBuf
|
||||
}
|
||||
|
||||
pub struct Gitignore {
|
||||
pub language: String,
|
||||
pub text: String
|
||||
}
|
||||
|
||||
impl GitignoreRepo {
|
||||
// NOTE: This is only declared due to ::new() being a standard in
|
||||
// the Rust ecosystem. This is really just an alias to `fetch_or_load`.
|
||||
pub fn new() -> Result<Self> {
|
||||
Self::initialize()
|
||||
}
|
||||
|
||||
fn fetch_and_return(into: &Path) -> Result<Self> {
|
||||
let pb = ProgressBar::new_spinner();
|
||||
pb.set_message(format!("Cloning from {}...", GITIGNORE_URL));
|
||||
pb.enable_steady_tick(Duration::from_millis(100));
|
||||
|
||||
Repository::clone(GITIGNORE_URL, into)
|
||||
.with_context(|| format!("Failed to clone `.gitignore` repo into {}", into.display()))?;
|
||||
|
||||
pb.finish_and_clear();
|
||||
|
||||
info!("Cloned from {}", GITIGNORE_URL);
|
||||
|
||||
Ok(Self { path: into.into() })
|
||||
}
|
||||
|
||||
fn initialize() -> Result<Self> {
|
||||
let dirs: ProjectDirs = ProjectDirs::from("dev", "teesh", "gig")
|
||||
.with_context(|| "ProjectDirs couldn't be initialized. Are you sure you have $HOME?")?;
|
||||
|
||||
if !dirs.data_dir().is_dir() {
|
||||
warn!(".gitignore directory does not exist! Fetching...");
|
||||
return Self::fetch_and_return(dirs.data_dir());
|
||||
}
|
||||
|
||||
Ok(Self { path: dirs.data_dir().into() })
|
||||
}
|
||||
|
||||
pub fn find(&self, name: String) -> Vec<String> {
|
||||
let pattern = format!("{}/**/{}*.gitignore", self.path.display(), name);
|
||||
|
||||
let options = MatchOptions {
|
||||
case_sensitive: false,
|
||||
require_literal_separator: false,
|
||||
require_literal_leading_dot: false
|
||||
};
|
||||
|
||||
glob::glob_with(&pattern, options)
|
||||
.map(|paths| {
|
||||
paths
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter_map(|path| path.to_str().map(String::from))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
// This function works like the following:
|
||||
// get_lang_from_file_name(".../Python.gitignore") -> "Python"
|
||||
// get_lang_from_file_name(".../Rails.Ruby.gitignore") -> "Rails.Ruby"
|
||||
// get_lang_from_file_name(".../Anything Goes.gitignore") -> "Anything goes"
|
||||
// get_lang_from_file_name(".../even.chinese") -> "even"
|
||||
pub fn get_lang_from_file_name(filename: String) -> String {
|
||||
Path::new(&filename)
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or(&filename)
|
||||
.to_string()
|
||||
}
|
||||
91
src/main.rs
Normal file
91
src/main.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
use figlet_rs::FIGfont;
|
||||
|
||||
use gig::{Args, Gitignore, GitignoreRepo, get_lang_from_file_name};
|
||||
use clap::Parser;
|
||||
use anyhow::{Result, Context};
|
||||
use dialoguer::Select;
|
||||
use tracing::{warn, info};
|
||||
|
||||
fn print_header() {
|
||||
let standard_font = FIGfont::standard().unwrap();
|
||||
let figure = standard_font.convert("gig");
|
||||
|
||||
if let Some(output) = figure {
|
||||
println!("{}", output);
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_gitignores(gitignores: Vec<Gitignore>) -> String {
|
||||
let mut merged = String::new();
|
||||
|
||||
for gitignore in gitignores {
|
||||
merged += format!("### {} ###\n", gitignore.language).as_str();
|
||||
merged += gitignore.text.as_str();
|
||||
merged += format!("### End of {} ###\n", gitignore.language).as_str();
|
||||
merged += "\n";
|
||||
}
|
||||
|
||||
merged
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
.with_target(false)
|
||||
.with_ansi(true)
|
||||
.init();
|
||||
|
||||
print_header();
|
||||
|
||||
let args = Args::parse();
|
||||
let repository: GitignoreRepo = GitignoreRepo::new()?;
|
||||
|
||||
let mut used: Vec<String> = vec![];
|
||||
for template in args.templates {
|
||||
let possible: Vec<String> = repository.find(template.clone());
|
||||
|
||||
let chose: String;
|
||||
if possible.len() > 1 {
|
||||
let selection: usize = Select::new()
|
||||
.with_prompt(format!("Which template do you mean by {}?", template))
|
||||
.items(&possible)
|
||||
.interact()
|
||||
.with_context(|| "Failed to do a `Select` prompt.")?;
|
||||
|
||||
chose = possible[selection].clone();
|
||||
} else if possible.is_empty() {
|
||||
warn!("No template found for {}! Skipping...", template);
|
||||
continue;
|
||||
} else {
|
||||
chose = possible[0].clone();
|
||||
}
|
||||
|
||||
used.push(chose);
|
||||
}
|
||||
|
||||
let gitignores: Vec<Gitignore> = used
|
||||
.into_iter()
|
||||
.filter_map(|path| {
|
||||
let text = std::fs::read_to_string(&path).ok()?;
|
||||
let language = get_lang_from_file_name(path);
|
||||
Some(Gitignore { language, text })
|
||||
})
|
||||
.collect();
|
||||
|
||||
let names: Vec<String> = gitignores.iter().map(|g| g.language.clone()).collect();
|
||||
let merged = merge_gitignores(gitignores);
|
||||
|
||||
std::fs::write(".gitignore", &merged)
|
||||
.with_context(|| "Failed to write to .gitignore")?;
|
||||
|
||||
let names: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
|
||||
let summary = match names.as_slice() {
|
||||
[] => String::new(),
|
||||
[a] => a.to_string(),
|
||||
[a, b] => format!("{} and {}", a, b),
|
||||
[rest @ .., last] => format!("{}, and {}", rest.join(", "), last),
|
||||
};
|
||||
info!("Created a .gitignore file with {}", summary);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue