use std::{path::PathBuf, process::Command, time::Instant};
use anyhow::Result;
use gix::bstr::ByteSlice;
fn get_staged_files_command(repo_path: &std::path::Path) -> Result<Vec<PathBuf>> {
let output = Command::new("git")
.args(["diff", "--cached", "--name-only"])
.current_dir(repo_path)
.output()?;
if !output.status.success() {
return Ok(Vec::new());
}
let stdout = String::from_utf8(output.stdout)?;
Ok(stdout
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| repo_path.join(line.trim()))
.collect())
}
fn get_staged_files_libgit2(repo: &git2::Repository) -> Result<Vec<PathBuf>> {
let mut opts = git2::StatusOptions::new();
opts.show(git2::StatusShow::Index)
.include_untracked(false)
.include_ignored(false);
let statuses = repo.statuses(Some(&mut opts))?;
let workdir = repo.workdir().unwrap();
let mut files = Vec::new();
for entry in statuses.iter() {
let status = entry.status();
if (status.contains(git2::Status::INDEX_NEW)
|| status.contains(git2::Status::INDEX_MODIFIED)
|| status.contains(git2::Status::INDEX_DELETED)
|| status.contains(git2::Status::INDEX_RENAMED)
|| status.contains(git2::Status::INDEX_TYPECHANGE))
&& let Some(path) = entry.path()
{
files.push(workdir.join(path));
}
}
Ok(files)
}
fn get_staged_files_gix(repo: &gix::Repository) -> Result<Vec<PathBuf>> {
let mut staged_files = Vec::new();
let workdir = repo.workdir().unwrap();
let index = repo.index()?;
let head_tree = match repo.head_tree_id() {
Ok(tree_id) => Some(repo.find_tree(tree_id)?),
Err(_) => None, };
if head_tree.is_none() {
for entry in index.entries() {
let path = entry.path(&index);
staged_files.push(workdir.join(path.to_path_lossy().as_ref()));
}
return Ok(staged_files);
}
let tree = head_tree.unwrap();
for entry in index.entries() {
let path = entry.path(&index);
let path_str = path.to_path_lossy();
match tree.lookup_entry_by_path(&*path_str) {
Ok(Some(tree_entry)) => {
if entry.id != tree_entry.object_id() {
staged_files.push(workdir.join(path.to_path_lossy().as_ref()));
}
}
Ok(None) | Err(_) => {
staged_files.push(workdir.join(path.to_path_lossy().as_ref()));
}
}
}
Ok(staged_files)
}
fn main() {
println!("\n=== Git Performance Comparison ===\n");
let repo_path = std::env::current_dir().unwrap();
println!("Warming up...");
for _ in 0..5 {
let _ = get_staged_files_command(&repo_path);
}
println!("\nTesting external git command:");
let mut command_times = Vec::new();
for i in 0..10 {
let start = Instant::now();
let files = get_staged_files_command(&repo_path).unwrap();
let elapsed = start.elapsed();
command_times.push(elapsed);
if i == 0 {
println!(" Files found: {}", files.len());
}
}
let command_avg =
command_times.iter().sum::<std::time::Duration>() / command_times.len() as u32;
println!(" Average time: {command_avg:?}");
if command_avg.as_millis() == 0 {
println!(" (in microseconds: {}μs)", command_avg.as_micros());
}
println!("\nTesting libgit2:");
let git2_repo = git2::Repository::discover(&repo_path).unwrap();
let mut libgit2_times = Vec::new();
for i in 0..10 {
let start = Instant::now();
let files = get_staged_files_libgit2(&git2_repo).unwrap();
let elapsed = start.elapsed();
libgit2_times.push(elapsed);
if i == 0 {
println!(" Files found: {}", files.len());
}
}
let libgit2_avg =
libgit2_times.iter().sum::<std::time::Duration>() / libgit2_times.len() as u32;
println!(" Average time: {libgit2_avg:?}");
if libgit2_avg.as_millis() == 0 {
println!(" (in microseconds: {}μs)", libgit2_avg.as_micros());
}
println!("\nTesting gix (gitoxide):");
let gix_repo = gix::discover(&repo_path).unwrap();
let mut gix_times = Vec::new();
for i in 0..10 {
let start = Instant::now();
let files = get_staged_files_gix(&gix_repo).unwrap();
let elapsed = start.elapsed();
gix_times.push(elapsed);
if i == 0 {
println!(" Files found: {}", files.len());
}
}
let gix_avg = gix_times.iter().sum::<std::time::Duration>() / gix_times.len() as u32;
println!(" Average time: {gix_avg:?}");
if gix_avg.as_millis() == 0 {
println!(" (in microseconds: {}μs)", gix_avg.as_micros());
}
println!("\n=== Performance Summary ===");
println!("Command: {command_avg:?}");
println!("libgit2: {libgit2_avg:?}");
println!("gix: {gix_avg:?}");
println!("\nSpeedup vs Command:");
println!(
" libgit2: {:.1}x faster",
command_avg.as_nanos() as f64 / libgit2_avg.as_nanos() as f64
);
println!(
" gix: {:.1}x faster",
command_avg.as_nanos() as f64 / gix_avg.as_nanos() as f64
);
println!("\nRecommendation:");
if libgit2_avg < gix_avg {
println!(" → Use libgit2 for best performance");
} else {
println!(" → Use gix for best performance (and it's pure Rust!)");
}
}