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/.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/.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" + } + } +} diff --git a/Cargo.lock b/Cargo.lock index bdaa49b..c552b26 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", ] @@ -672,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 c2a730a..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" @@ -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/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 diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..2a6f43f --- /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 + 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 e7a11a9..48b34a4 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::Tree(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(()) } 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(()) +}