git-slop 0.16.0

A deterministic repository token-defragmenter for humans and AI agents.
Documentation
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};

use globset::Glob;
use serde_json::Value;

fn candidate_directories(candidates: &[String]) -> Vec<PathBuf> {
    let mut directories = BTreeSet::from([PathBuf::new()]);
    for candidate in candidates {
        let mut parent = Path::new(candidate).parent();
        while let Some(path) = parent {
            directories.insert(path.to_path_buf());
            parent = path.parent();
        }
    }
    directories.into_iter().collect()
}

fn commands_for_directory(
    directory: &Path,
    exists: impl Fn(&Path) -> bool,
    has_dotnet_project: impl Fn(&Path) -> bool,
) -> Vec<&'static str> {
    if exists(&directory.join("Cargo.toml")) {
        vec![
            "cargo fmt --all -- --check",
            "cargo clippy --all-targets --all-features -- -D warnings",
            "cargo test --all-targets",
        ]
    } else if exists(&directory.join("go.mod")) {
        vec!["go test ./..."]
    } else if exists(&directory.join("pyproject.toml")) {
        vec!["pytest"]
    } else if exists(&directory.join("package.json")) {
        if exists(&directory.join("bun.lock")) || exists(&directory.join("bun.lockb")) {
            vec!["bun test"]
        } else if exists(&directory.join("pnpm-lock.yaml")) {
            vec!["pnpm test"]
        } else if exists(&directory.join("yarn.lock")) {
            vec!["yarn test"]
        } else {
            vec!["npm test"]
        }
    } else if exists(&directory.join("pom.xml")) {
        vec!["mvn test"]
    } else if exists(&directory.join("gradlew")) {
        vec!["./gradlew test"]
    } else if has_dotnet_project(directory) {
        vec!["dotnet test"]
    } else if exists(&directory.join("Makefile")) {
        vec!["make test"]
    } else {
        Vec::new()
    }
}

fn command_family(command: &str) -> String {
    let command = command
        .rsplit_once("&&")
        .map_or(command, |(_, command)| command)
        .trim();
    let words = command.split_whitespace().collect::<Vec<_>>();
    match words.as_slice() {
        ["cargo", operation, ..] if matches!(*operation, "fmt" | "clippy" | "test") => {
            format!("cargo {operation}")
        }
        [runner, "test", ..] if matches!(*runner, "bun" | "npm" | "pnpm" | "yarn" | "go") => {
            format!("{runner} test")
        }
        ["pytest", ..] => "pytest".to_string(),
        ["mvn", "test", ..] => "mvn test".to_string(),
        ["dotnet", "test", ..] => "dotnet test".to_string(),
        _ => command.to_string(),
    }
}

fn discover(
    candidates: &[String],
    configured_commands: &[String],
    exists: impl Fn(&Path) -> bool + Copy,
    has_dotnet_project: impl Fn(&Path) -> bool + Copy,
) -> Vec<String> {
    let mut commands = configured_commands
        .iter()
        .filter(|command| !command.trim().is_empty())
        .cloned()
        .collect::<Vec<_>>();
    let configured_families = commands
        .iter()
        .map(|command| command_family(command))
        .collect::<BTreeSet<_>>();
    for directory in candidate_directories(candidates) {
        let prefix = if directory.as_os_str().is_empty() {
            String::new()
        } else {
            format!("cd {} && ", directory.to_string_lossy())
        };
        for command in commands_for_directory(&directory, exists, has_dotnet_project) {
            let command = format!("{prefix}{command}");
            if !configured_families.contains(&command_family(&command))
                && !commands.contains(&command)
            {
                commands.push(command);
            }
        }
    }
    commands
}

pub(super) fn from_worktree(
    root: &Path,
    candidates: &[String],
    configured_commands: &[String],
) -> Vec<String> {
    discover(
        candidates,
        configured_commands,
        |path| root.join(path).is_file(),
        |directory| {
            std::fs::read_dir(root.join(directory))
                .ok()
                .is_some_and(|entries| {
                    entries.filter_map(Result::ok).any(|entry| {
                        matches!(
                            entry.path().extension().and_then(|value| value.to_str()),
                            Some("sln" | "csproj")
                        )
                    })
                })
        },
    )
}

pub(super) fn from_report_paths(
    report: &Value,
    repository_paths: &BTreeSet<String>,
    candidates: &[String],
    configured_commands: &[String],
) -> Vec<String> {
    let mut commands = discover(
        candidates,
        configured_commands,
        |path| repository_paths.contains(&path.to_string_lossy().replace('\\', "/")),
        |directory| {
            let prefix = if directory.as_os_str().is_empty() {
                String::new()
            } else {
                format!("{}/", directory.to_string_lossy().replace('\\', "/"))
            };
            repository_paths.iter().any(|path| {
                path.strip_prefix(&prefix)
                    .filter(|relative| !relative.contains('/'))
                    .is_some_and(|relative| {
                        relative.ends_with(".sln") || relative.ends_with(".csproj")
                    })
            })
        },
    );
    let mut push = |command: String| {
        if !command.trim().is_empty() && !commands.contains(&command) {
            commands.push(command);
        }
    };
    for mapping in report
        .pointer("/config/verification/path_commands")
        .and_then(Value::as_array)
        .into_iter()
        .flatten()
    {
        let Some(pattern) = mapping.get("path_glob").and_then(Value::as_str) else {
            continue;
        };
        let Some(command) = mapping.get("command").and_then(Value::as_str) else {
            continue;
        };
        if Glob::new(pattern).ok().is_some_and(|glob| {
            let matcher = glob.compile_matcher();
            candidates.iter().any(|path| matcher.is_match(path))
        }) {
            push(command.to_owned());
        }
    }
    for path in candidates {
        let Some(record) = report
            .get("files")
            .and_then(Value::as_array)
            .and_then(|files| files.iter().find(|record| record["path"] == *path))
        else {
            continue;
        };
        if let Some(command) = record
            .pointer("/generated_provenance/verification_command")
            .and_then(Value::as_str)
        {
            push(command.to_owned());
        }
        for test in record
            .pointer("/overlays/verification/nearby_test_paths")
            .and_then(Value::as_array)
            .into_iter()
            .flatten()
            .filter_map(Value::as_str)
        {
            if let Some(name) = test
                .strip_prefix("tests/")
                .and_then(|path| path.strip_suffix(".rs"))
                .filter(|path| !path.contains('/'))
            {
                push(format!("cargo test --test {name}"));
            }
        }
    }
    commands
}

#[cfg(test)]
mod tests {
    use super::from_report_paths;
    use std::collections::BTreeSet;

    #[test]
    fn discovers_nested_package_managers_and_build_systems() {
        let paths = [
            "Cargo.toml",
            "packages/web/package.json",
            "packages/web/pnpm-lock.yaml",
            "services/api/pom.xml",
        ]
        .into_iter()
        .map(str::to_owned)
        .collect::<BTreeSet<_>>();
        let commands = from_report_paths(
            &serde_json::json!({}),
            &paths,
            &[
                "packages/web/src/app.ts".into(),
                "services/api/src/Main.java".into(),
            ],
            &["./scripts/verify-contracts.sh".into()],
        );
        assert!(commands.contains(&"cargo test --all-targets".to_string()));
        assert!(commands.contains(&"cd packages/web && pnpm test".to_string()));
        assert!(commands.contains(&"cd services/api && mvn test".to_string()));
        assert!(commands.contains(&"./scripts/verify-contracts.sh".to_string()));
    }

    #[test]
    fn configured_command_families_replace_generic_autodetection() {
        let paths = ["Cargo.toml"]
            .into_iter()
            .map(str::to_owned)
            .collect::<BTreeSet<_>>();
        let commands = from_report_paths(
            &serde_json::json!({}),
            &paths,
            &["src/contract.rs".into()],
            &[
                "cargo fmt --all -- --check".into(),
                "cargo clippy --workspace --all-targets --all-features --locked -- -D warnings"
                    .into(),
                "cargo test --workspace --all-targets --all-features --locked".into(),
            ],
        );
        assert_eq!(commands.len(), 3);
        assert!(
            !commands
                .iter()
                .any(|command| command == "cargo test contract")
        );
        assert!(
            !commands
                .iter()
                .any(|command| command == "cargo test --all-targets")
        );
    }
}