alef 0.110.5

Opinionated polyglot binding generator for Rust libraries
Documentation
use std::collections::HashSet;
use std::path::{Component, Path, PathBuf};
use std::process::Command;

pub(super) fn paths(base: &Path) -> HashSet<PathBuf> {
    let mut paths = HashSet::new();
    if base.join(".gitmodules").is_file()
        && let Some(output) = git_output(
            base,
            &[
                "config",
                "--file",
                ".gitmodules",
                "--null",
                "--get-regexp",
                r"^submodule\..*\.path$",
            ],
        )
    {
        for record in output.split(|&byte| byte == 0) {
            if let Some(index) = record.iter().position(|&byte| byte == b'\n') {
                insert_relative(&mut paths, &record[index + 1..]);
            }
        }
    }
    if let Some(output) = git_output(base, &["ls-files", "--stage", "-z", "--", "."]) {
        for record in output.split(|&byte| byte == 0) {
            if record.starts_with(b"160000 ")
                && let Some(index) = record.iter().position(|&byte| byte == b'\t')
            {
                insert_relative(&mut paths, &record[index + 1..]);
            }
        }
    }
    paths
}

fn git_output(base: &Path, args: &[&str]) -> Option<Vec<u8>> {
    match Command::new("git").arg("-C").arg(base).args(args).output() {
        Ok(output) if output.status.success() => Some(output.stdout),
        Ok(output) => {
            tracing::debug!(path = %base.display(), operation = args[0], status = %output.status, "submodule metadata query unavailable; retaining filesystem scan");
            None
        }
        Err(error) => {
            tracing::warn!(path = %base.display(), operation = args[0], %error, "reading submodule metadata failed; retaining filesystem scan");
            None
        }
    }
}

fn insert_relative(paths: &mut HashSet<PathBuf>, bytes: &[u8]) {
    let Ok(text) = std::str::from_utf8(bytes) else {
        return;
    };
    let path = Path::new(text);
    if !text.is_empty()
        && path
            .components()
            .all(|part| matches!(part, Component::Normal(_) | Component::CurDir))
    {
        paths.insert(path.components().filter(|part| *part != Component::CurDir).collect());
    }
}