From df73afb879c60c84bbca2710cf7fbb2136c56d2a Mon Sep 17 00:00:00 2001 From: "Joseph D. Carpinelli" Date: Wed, 25 Feb 2026 21:57:51 -0500 Subject: [PATCH 1/4] feat: add `git-filter` CLI --- Cargo.lock | 2 ++ Cargo.toml | 1 + README.md | 16 ++++++++++++ src/cli.rs | 16 ++++++++++++ src/main.rs | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 README.md create mode 100644 src/cli.rs diff --git a/Cargo.lock b/Cargo.lock index bdaa49b..3deaf9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -195,6 +195,7 @@ version = "0.0.0" dependencies = [ "clap", "clap_mangen", + "git-filter-tree", "git2", "globset", ] @@ -203,6 +204,7 @@ dependencies = [ name = "git-filter-tree" version = "0.0.0" dependencies = [ + "clap", "git2", "globset", ] diff --git a/Cargo.toml b/Cargo.toml index c2a730a..32b9c4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ path = "src/main.rs" clap.workspace = true git2.workspace = true globset.workspace = true +git-filter-tree = { path = "plumbing/git-filter-tree" } [build-dependencies] clap_mangen.workspace = true diff --git a/README.md b/README.md new file mode 100644 index 0000000..0330cc1 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# ✂️ `git-vendor` + +*An in-source vendoring alternative to Git submodules and subtrees.* + + +[![CI](https://github.com/joeycarpinelli/git-vendor/actions/workflows/CI.yml/badge.svg)](https://github.com/joeycarpinelli/git-vendor/actions/workflows/CI.yml) +[![CD](https://github.com/joeycarpinelli/git-vendor/actions/workflows/CD.yml/badge.svg)](https://github.com/joeycarpinelli/git-vendor/actions/workflows/CD.yml) + + +## Overview + +*More documentation is en route!* + +## Installation + +*Installation instructions are en route!* diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..37c551a --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,16 @@ +use clap::Parser; +use git_filter_tree::cli::FilterTreeArgs; + +#[derive(Parser)] +#[command(name = "git-filter")] +#[command(author, version, about = "Filter Git repository history, trees, and blobs", long_about = None)] +pub struct Cli { + #[command(subcommand)] + pub command: Command, +} + +#[derive(clap::Subcommand)] +pub enum Command { + /// Filter Git tree entries by gitattributes-style patterns + FilterTree(FilterTreeArgs), +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..48edee0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,75 @@ +mod cli; + +use clap::Parser; +use cli::{Cli, Command}; +use git_filter_tree::FilterTree; +use git_filter_tree::cli::OutputFormat; +use git2 as git; +use std::process; + fn main() { - println!("Hello, world!"); + if let Err(e) = run() { + eprintln!("Error: {}", e); + process::exit(1); + } +} + +fn run() -> Result<(), Box> { + let cli = Cli::parse(); + + match cli.command { + Command::FilterTree(args) => filter_tree(&args.treeish, &args.patterns, args.format), + } +} + +fn filter_tree( + treeish: &str, + patterns: &[String], + format: OutputFormat, +) -> Result<(), Box> { + let repo = git::Repository::open_from_env()?; + + let obj = repo.revparse_single(treeish)?; + let tree = obj.peel_to_tree()?; + + let patterns: Vec<&str> = patterns.iter().map(|s| s.as_str()).collect(); + + let filtered_tree = repo.filter_by_patterns(&tree, &patterns)?; + + match format { + OutputFormat::TreeSha => { + println!("{}", filtered_tree.id()); + } + OutputFormat::Entries => { + for entry in filtered_tree.iter() { + let name = entry.name().unwrap_or(""); + let kind = match entry.kind() { + Some(git::ObjectType::Blob) => "blob", + Some(git::ObjectType::Tree) => "tree", + Some(git::ObjectType::Commit) => "commit", + _ => "unknown", + }; + println!("{}\t{}", kind, name); + } + } + OutputFormat::Detailed => { + println!("Tree: {}", filtered_tree.id()); + println!("Entries: {}", filtered_tree.len()); + println!(); + for entry in filtered_tree.iter() { + let name = entry.name().unwrap_or(""); + let kind = match entry.kind() { + Some(git::ObjectType::Blob) => "blob", + Some(git::ObjectType::Tree) => "tree", + Some(git::ObjectType::Commit) => "commit", + _ => "unknown", + }; + let mode = entry.filemode(); + let id = entry.id(); + println!("{:06o} {} {}\t{}", mode, kind, id, name); + } + } + } + + Ok(()) } From 3c3d5c2d69fd457418a6fe6c4e2120c69b7f9e56 Mon Sep 17 00:00:00 2001 From: "Joseph D. Carpinelli" Date: Wed, 25 Feb 2026 22:26:53 -0500 Subject: [PATCH 2/4] feat: add man-page generation via `clap_mangen` Generated-by: Zed (Claude Sonet 4.6) --- .cargo/config.toml | 2 + Cargo.lock | 10 +++ Cargo.toml | 2 +- src/cli.rs | 2 +- src/lib.rs | 6 +- src/main.rs | 2 +- xtask/Cargo.toml | 12 ++++ xtask/src/main.rs | 172 +++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 201 insertions(+), 7 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 xtask/Cargo.toml create mode 100644 xtask/src/main.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..35049cb --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +xtask = "run --package xtask --" diff --git a/Cargo.lock b/Cargo.lock index 3deaf9a..c552b26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -674,6 +674,16 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "xtask" +version = "0.0.0" +dependencies = [ + "clap", + "clap_mangen", + "git-filter", + "git-filter-tree", +] + [[package]] name = "yoke" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index 32b9c4c..4930bf8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = [".", "plumbing/git-filter-tree"] +members = [".", "plumbing/git-filter-tree", "xtask"] [workspace.package] edition = "2024" diff --git a/src/cli.rs b/src/cli.rs index 37c551a..2a6f43f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -12,5 +12,5 @@ pub struct Cli { #[derive(clap::Subcommand)] pub enum Command { /// Filter Git tree entries by gitattributes-style patterns - FilterTree(FilterTreeArgs), + Tree(FilterTreeArgs), } diff --git a/src/lib.rs b/src/lib.rs index b85dc30..2f416ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,3 @@ -///! Filter repository history, trees, and (eventually) blobs. +//! Filter repository history, trees, and (eventually) blobs. -pub fn add(x: i32, y: i32) -> i32 { - x + y -} +pub mod cli; diff --git a/src/main.rs b/src/main.rs index 48edee0..48b34a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,7 @@ fn run() -> Result<(), Box> { let cli = Cli::parse(); match cli.command { - Command::FilterTree(args) => filter_tree(&args.treeish, &args.patterns, args.format), + Command::Tree(args) => filter_tree(&args.treeish, &args.patterns, args.format), } } diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 0000000..19f3db4 --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "xtask" +version = "0.0.0" +edition.workspace = true +publish = false +license.workspace = true + +[dependencies] +clap.workspace = true +clap_mangen.workspace = true +git-filter-tree = { path = "../plumbing/git-filter-tree" } +git-filter = { path = "../" } diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 0000000..6803f83 --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,172 @@ +use clap::CommandFactory; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +#[derive(clap::Parser)] +#[command(name = "xtask")] +#[command(about = "Development tasks for git-filter")] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(clap::Subcommand)] +enum Commands { + /// Build all crates and generate man pages into the target profile directory + Build { + /// Build with the release profile + #[arg(short, long)] + release: bool, + + /// Build with a named profile (overrides --release) + #[arg(long, value_name = "PROFILE")] + profile: Option, + + /// Target directory (default: workspace target/) + #[arg(long, value_name = "DIR")] + target_dir: Option, + }, + + /// Generate man pages for all CLI tools + GenMan { + /// Output directory for man pages (a man1/ subdirectory will be created inside) + #[arg(short, long, default_value = "target/debug/man")] + output: PathBuf, + }, +} + +fn main() { + let cli = clap::Parser::parse(); + + let result = match cli { + Cli { + command: + Commands::Build { + release, + profile, + target_dir, + }, + } => run_build(release, profile, target_dir), + + Cli { + command: Commands::GenMan { output }, + } => generate_man_pages(&output), + }; + + if let Err(e) = result { + eprintln!("Error: {}", e); + std::process::exit(1); + } +} + +// --------------------------------------------------------------------------- +// Build +// --------------------------------------------------------------------------- + +fn run_build( + release: bool, + profile: Option, + target_dir: Option, +) -> std::io::Result<()> { + // Resolve the effective profile name so we know where binaries land. + let profile_name = profile.clone().unwrap_or_else(|| { + if release { + "release".into() + } else { + "debug".into() + } + }); + + // Build the `cargo build` invocation, excluding xtask itself. + let mut cmd = Command::new(cargo_bin()); + cmd.arg("build") + .arg("--workspace") + .arg("--exclude") + .arg("xtask"); + + if let Some(ref p) = profile { + cmd.arg("--profile").arg(p); + } else if release { + cmd.arg("--release"); + } + + let resolved_target_dir = if let Some(ref dir) = target_dir { + cmd.arg("--target-dir").arg(dir); + dir.clone() + } else { + // Default: /target + workspace_root()?.join("target") + }; + + println!("Running: {:?}", cmd); + + let status = cmd.status()?; + if !status.success() { + return Err(std::io::Error::other(format!( + "cargo build failed with status {}", + status + ))); + } + + // Generate man pages next to the built binaries: + // //man/ + let man_dir = resolved_target_dir.join(&profile_name).join("man"); + generate_man_pages(&man_dir)?; + + println!("\nView with: MANPATH={} man git-filter", man_dir.display()); + Ok(()) +} + +/// Returns the path to the `cargo` executable, preferring `$CARGO` when set +/// (Cargo sets this env-var when invoking subprocesses so the same toolchain +/// is used). +fn cargo_bin() -> String { + std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()) +} + +/// Walk up from the xtask binary's location to find the workspace root, i.e. +/// the directory that contains the top-level `Cargo.toml`. Falls back to the +/// current working directory when the manifest cannot be located. +fn workspace_root() -> std::io::Result { + // `cargo run -p xtask` sets CARGO_MANIFEST_DIR to the xtask package dir. + // The workspace root is one level up from there. + if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") { + let xtask_dir = PathBuf::from(manifest_dir); + if let Some(parent) = xtask_dir.parent() { + return Ok(parent.to_path_buf()); + } + } + std::env::current_dir() +} + +// --------------------------------------------------------------------------- +// Man-page generation +// --------------------------------------------------------------------------- + +fn generate_man_pages(output_dir: &Path) -> std::io::Result<()> { + let man1_dir = output_dir.join("man1"); + fs::create_dir_all(&man1_dir)?; + + println!("Generating man pages to: {}", man1_dir.display()); + + generate_man::(&man1_dir, "git-filter-tree")?; + generate_man::(&man1_dir, "git-filter")?; + + println!("✓ Man pages generated successfully!"); + Ok(()) +} + +fn generate_man(output_dir: &Path, name: &str) -> std::io::Result<()> { + let cmd = C::command(); + let man = clap_mangen::Man::new(cmd); + let mut buffer = Vec::new(); + man.render(&mut buffer)?; + + let filename = format!("{}.1", name); + let man_path = output_dir.join(&filename); + fs::write(&man_path, buffer)?; + + println!(" → {}", filename); + Ok(()) +} From ab05345678ca38c75ecd7ad9c2e2a563e2d7ed69 Mon Sep 17 00:00:00 2001 From: "Joseph D. Carpinelli" Date: Wed, 25 Feb 2026 22:27:31 -0500 Subject: [PATCH 3/4] chore: add Zed settings for simpler `git` invocations After building, run `git filter` like a user would! --- .zed/settings.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .zed/settings.json diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..f7b4cb0 --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,8 @@ +{ + "terminal": { + "env": { + "PATH": "target/debug:$PATH", + "MANPATH": "$PWD/target/share/man:$MANPATH" + } + } +} From 76a67199edf58468b38ede3fff8400ac81e612ed Mon Sep 17 00:00:00 2001 From: "robot-yavanna[bot]" <264059672+robot-yavanna[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 03:28:18 +0000 Subject: [PATCH 4/4] chore(release): git-filter-tree-v0.1.0 --- .config/release-please-manifest.json | 2 +- plumbing/git-filter-tree/CHANGELOG.md | 9 +++++++++ plumbing/git-filter-tree/Cargo.toml | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 plumbing/git-filter-tree/CHANGELOG.md diff --git a/.config/release-please-manifest.json b/.config/release-please-manifest.json index f7200b9..f373b0e 100644 --- a/.config/release-please-manifest.json +++ b/.config/release-please-manifest.json @@ -1,4 +1,4 @@ { ".": "0.0.0", - "plumbing/git-filter-tree": "0.0.0" + "plumbing/git-filter-tree": "0.1.0" } diff --git a/plumbing/git-filter-tree/CHANGELOG.md b/plumbing/git-filter-tree/CHANGELOG.md new file mode 100644 index 0000000..c23f617 --- /dev/null +++ b/plumbing/git-filter-tree/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## 0.1.0 (2026-02-26) + + +### Features + +* Add CLI to `git-filter-tree` ([d2c4e9d](https://github.com/git-ents/git-filter/commit/d2c4e9d3cad71d1281fcade6373d2d9f3252fa2c)) +* Add library crate: `git-filter-tree` ([21acf6d](https://github.com/git-ents/git-filter/commit/21acf6d206ca2af0b4726ba533fd7627d2a20a98)) diff --git a/plumbing/git-filter-tree/Cargo.toml b/plumbing/git-filter-tree/Cargo.toml index 7eb96c1..c45c260 100644 --- a/plumbing/git-filter-tree/Cargo.toml +++ b/plumbing/git-filter-tree/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "git-filter-tree" -version = "0.0.0" +version = "0.1.0" edition.workspace = true publish.workspace = true license.workspace = true