Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[alias]
xtask = "run --package xtask --"
2 changes: 1 addition & 1 deletion .config/release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
".": "0.0.0",
"plumbing/git-filter-tree": "0.0.0"
"plumbing/git-filter-tree": "0.1.0"
}
8 changes: 8 additions & 0 deletions .zed/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"terminal": {
"env": {
"PATH": "target/debug:$PATH",
"MANPATH": "$PWD/target/share/man:$MANPATH"
}
}
}
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[workspace]
resolver = "3"
members = [".", "plumbing/git-filter-tree"]
members = [".", "plumbing/git-filter-tree", "xtask"]

[workspace.package]
edition = "2024"
Expand Down Expand Up @@ -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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# ✂️ `git-vendor`

*An in-source vendoring alternative to Git submodules and subtrees.*

<!-- rumdl-disable MD013 -->
[![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)
<!-- rumdl-enable MD013 -->

## Overview

*More documentation is en route!*

## Installation

*Installation instructions are en route!*
9 changes: 9 additions & 0 deletions plumbing/git-filter-tree/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
2 changes: 1 addition & 1 deletion plumbing/git-filter-tree/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
16 changes: 16 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -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),
}
6 changes: 2 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
74 changes: 73 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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("<invalid-utf8>");
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("<invalid-utf8>");
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(())
}
12 changes: 12 additions & 0 deletions xtask/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 = "../" }
172 changes: 172 additions & 0 deletions xtask/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<String>,

/// Target directory (default: workspace target/)
#[arg(long, value_name = "DIR")]
target_dir: Option<PathBuf>,
},

/// 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<String>,
target_dir: Option<PathBuf>,
) -> 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: <workspace-root>/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:
// <target-dir>/<profile>/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<PathBuf> {
// `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::<git_filter_tree::cli::Cli>(&man1_dir, "git-filter-tree")?;
generate_man::<git_filter::cli::Cli>(&man1_dir, "git-filter")?;

println!("✓ Man pages generated successfully!");
Ok(())
}

fn generate_man<C: CommandFactory>(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(())
}