init: gig, because gitignores are boring

This commit is contained in:
Teesh 2026-02-11 22:16:04 +02:00
commit ee7171a766
12 changed files with 1449 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

33
.gitignore vendored Normal file
View 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

File diff suppressed because it is too large Load diff

16
Cargo.toml Normal file
View 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
View file

@ -0,0 +1,3 @@
# `gig` - the .gitignore cli
no docs yet, sorry

77
flake.lock generated Normal file
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,5 @@
{...}: {
systems = [
"x86_64-linux"
];
}

93
src/lib.rs Normal file
View 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
View 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(())
}