Skip to main content

ckg_git/
lib.rs

1//! Git helpers used by the indexer and blast-radius recency weighting.
2
3use std::path::Path;
4
5use ckg_core::Result;
6
7/// Resolve HEAD sha for the repo at `path`, or empty string if not a repo.
8///
9/// Errors are demoted to empty string so non-git directories index cleanly.
10/// We log at `debug` so operators can distinguish "not a repo" (expected)
11/// from "broken refs / unborn HEAD / detached HEAD" (worth investigating)
12/// when a repo unexpectedly has no recency weighting.
13///
14/// ## M4: `open` not `discover`
15///
16/// Uses `Repository::open` instead of `Repository::discover` so that
17/// only the exact directory `path` is opened as a git repo. `discover`
18/// walks parent directories: calling it on a subdirectory of a repo
19/// returns the repo root, so the recency weighting would silently apply
20/// to a different (larger) scope. `open` returns an error for
21/// non-root paths, which we demote to empty-string as for non-repos.
22///
23/// ## L8: `Result<String>` is retained
24///
25/// The function currently always returns `Ok(…)` — errors are demoted
26/// to `Ok(String::new())`. The `Result` wrapper is kept for two reasons:
27/// (a) callers already propagate it with `?`, changing the return type
28/// is a silent breaking change; (b) a future version may propagate IO
29/// errors (e.g. permission denied reading `.git/HEAD`) to let callers
30/// decide the error policy.
31pub fn head_sha(path: &Path) -> Result<String> {
32    match git2::Repository::open(path) {
33        Ok(repo) => {
34            // Post-check M4: verify workdir matches `path` exactly.
35            // `open` on a `.git` directory returns a bare-like handle
36            // with no workdir; `open` on the repo root returns the root.
37            if let Some(workdir) = repo.workdir() {
38                let canonical_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
39                let canonical_workdir = workdir.canonicalize().unwrap_or_else(|_| workdir.to_path_buf());
40                if canonical_path != canonical_workdir {
41                    tracing::debug!(
42                        "head_sha: {path} opened repo with different workdir {workdir}; skipping",
43                        path = path.display(),
44                        workdir = workdir.display(),
45                    );
46                    return Ok(String::new());
47                }
48            }
49            match repo.head().and_then(|h| h.peel_to_commit()) {
50                Ok(commit) => Ok(commit.id().to_string()),
51                Err(e) => {
52                    tracing::debug!(
53                        "head_sha: repo at {} has no peelable HEAD: {e}",
54                        path.display()
55                    );
56                    Ok(String::new())
57                }
58            }
59        }
60        Err(e) => {
61            tracing::debug!("head_sha: not a git repo at {}: {e}", path.display());
62            Ok(String::new())
63        }
64    }
65}