git-slop 0.11.3

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

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 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 = Vec::new();
    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 !commands.contains(&command) {
                commands.push(command);
            }
        }
    }
    for command in configured_commands {
        if !command.trim().is_empty() && !commands.contains(command) {
            commands.push(command.clone());
        }
    }
    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(
    repository_paths: &BTreeSet<String>,
    candidates: &[String],
    configured_commands: &[String],
) -> Vec<String> {
    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")
                    })
            })
        },
    )
}

#[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(
            &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()));
    }
}