ckg-git 1.3.1

Git introspection helpers (HEAD sha, recency).
Documentation
//! Git helpers used by the indexer and blast-radius recency weighting.

use std::path::Path;

use ckg_core::Result;

/// Resolve HEAD sha for the repo at `path`, or empty string if not a repo.
///
/// Errors are demoted to empty string so non-git directories index cleanly.
/// We log at `debug` so operators can distinguish "not a repo" (expected)
/// from "broken refs / unborn HEAD / detached HEAD" (worth investigating)
/// when a repo unexpectedly has no recency weighting.
///
/// ## M4: `open` not `discover`
///
/// Uses `Repository::open` instead of `Repository::discover` so that
/// only the exact directory `path` is opened as a git repo. `discover`
/// walks parent directories: calling it on a subdirectory of a repo
/// returns the repo root, so the recency weighting would silently apply
/// to a different (larger) scope. `open` returns an error for
/// non-root paths, which we demote to empty-string as for non-repos.
///
/// ## L8: `Result<String>` is retained
///
/// The function currently always returns `Ok(…)` — errors are demoted
/// to `Ok(String::new())`. The `Result` wrapper is kept for two reasons:
/// (a) callers already propagate it with `?`, changing the return type
/// is a silent breaking change; (b) a future version may propagate IO
/// errors (e.g. permission denied reading `.git/HEAD`) to let callers
/// decide the error policy.
pub fn head_sha(path: &Path) -> Result<String> {
    match git2::Repository::open(path) {
        Ok(repo) => {
            // Post-check M4: verify workdir matches `path` exactly.
            // `open` on a `.git` directory returns a bare-like handle
            // with no workdir; `open` on the repo root returns the root.
            if let Some(workdir) = repo.workdir() {
                let canonical_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
                let canonical_workdir = workdir.canonicalize().unwrap_or_else(|_| workdir.to_path_buf());
                if canonical_path != canonical_workdir {
                    tracing::debug!(
                        "head_sha: {path} opened repo with different workdir {workdir}; skipping",
                        path = path.display(),
                        workdir = workdir.display(),
                    );
                    return Ok(String::new());
                }
            }
            match repo.head().and_then(|h| h.peel_to_commit()) {
                Ok(commit) => Ok(commit.id().to_string()),
                Err(e) => {
                    tracing::debug!(
                        "head_sha: repo at {} has no peelable HEAD: {e}",
                        path.display()
                    );
                    Ok(String::new())
                }
            }
        }
        Err(e) => {
            tracing::debug!("head_sha: not a git repo at {}: {e}", path.display());
            Ok(String::new())
        }
    }
}