init: gig, because gitignores are boring
This commit is contained in:
commit
ee7171a766
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