stax 0.96.3

Fast stacked Git branches and PRs
Documentation
use std::path::Path;
use std::process::{Command, ExitStatus, Output};
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::time::Instant;

static TRACE_ENABLED: AtomicBool = AtomicBool::new(false);
static COMMAND_COUNT: AtomicUsize = AtomicUsize::new(0);
static TOTAL_MICROS: AtomicU64 = AtomicU64::new(0);

pub(crate) struct TraceGuard {
    enabled: bool,
    started_at: Instant,
}

impl TraceGuard {
    pub(crate) fn start(enabled: bool) -> Self {
        if enabled {
            COMMAND_COUNT.store(0, Ordering::Relaxed);
            TOTAL_MICROS.store(0, Ordering::Relaxed);
            TRACE_ENABLED.store(true, Ordering::Release);
        }
        Self {
            enabled,
            started_at: Instant::now(),
        }
    }
}

impl Drop for TraceGuard {
    fn drop(&mut self) {
        if !self.enabled {
            return;
        }
        TRACE_ENABLED.store(false, Ordering::Release);
        let count = COMMAND_COUNT.load(Ordering::Relaxed);
        let git_ms = TOTAL_MICROS.load(Ordering::Relaxed) as f64 / 1_000.0;
        let wall_ms = self.started_at.elapsed().as_secs_f64() * 1_000.0;
        eprintln!(
            "[trace] {count} git commands in {git_ms:.1}ms; command wall time {wall_ms:.1}ms"
        );
    }
}

pub(crate) fn output(cwd: &Path, args: &[&str]) -> std::io::Result<Output> {
    timed(cwd, args, || {
        Command::new("git").args(args).current_dir(cwd).output()
    })
}

pub(crate) fn status(cwd: &Path, args: &[&str]) -> std::io::Result<ExitStatus> {
    timed(cwd, args, || {
        Command::new("git").args(args).current_dir(cwd).status()
    })
}

fn timed<T>(
    cwd: &Path,
    args: &[&str],
    operation: impl FnOnce() -> std::io::Result<T>,
) -> std::io::Result<T> {
    let started_at = Instant::now();
    let result = operation();
    if TRACE_ENABLED.load(Ordering::Acquire) {
        let elapsed = started_at.elapsed();
        let number = COMMAND_COUNT.fetch_add(1, Ordering::Relaxed) + 1;
        TOTAL_MICROS.fetch_add(elapsed.as_micros() as u64, Ordering::Relaxed);
        let command = args
            .iter()
            .map(|arg| redact_arg(arg))
            .collect::<Vec<_>>()
            .join(" ");
        eprintln!(
            "[trace] git #{number} {:.1}ms @ {}: git {command}",
            elapsed.as_secs_f64() * 1_000.0,
            cwd.display()
        );
    }
    result
}

fn redact_arg(arg: &str) -> &str {
    if arg.contains("://") || arg.to_ascii_lowercase().contains("token=") {
        "<redacted>"
    } else {
        arg
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn trace_redacts_urls_and_tokens() {
        assert_eq!(redact_arg("https://token@example.com/repo"), "<redacted>");
        assert_eq!(redact_arg("token=secret"), "<redacted>");
        assert_eq!(redact_arg("feature/safe"), "feature/safe");
    }
}