Files
kebab/crates/kebab-parse-code/src/repo.rs
th-kim0823 ff11f81f7f feat(p10-1a-1): kebab-parse-code crate (lang + repo + skip)
Tasks 5-8: new `kebab-parse-code` crate with three infrastructure modules
for the code ingest framework. Ships lang.rs (extension→language identifier
mapping), repo.rs (.git walk-up via gix 0.70 for RepoMeta), and skip.rs
(BUILTIN_BLACKLIST, is_generated_file, is_oversized). 14 integration tests
across three test files, all passing; clippy -D warnings clean.

Note: gix pinned to 0.70 (not 0.83 as originally suggested) because 0.83
fails to compile against Rust 1.94.1 due to non-exhaustive match patterns
in gix-hash. 0.70 resolves cleanly and has identical head_name/head_id API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 15:57:59 +09:00

62 lines
2.1 KiB
Rust

//! Git repo auto-detection (spec §5.1).
//!
//! Walks up from `path` looking for a `.git/` directory. If found, reads
//! repo dir name, current branch, and HEAD commit using `gix` (pure Rust;
//! no `git` binary on PATH required).
use std::path::Path;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RepoMeta {
pub name: String,
pub branch: Option<String>,
pub commit: Option<String>,
}
/// Walk up from `path` until a `.git/` directory is found. Returns repo
/// metadata, or `None` if no repo boundary is reached before the filesystem
/// root.
///
/// - `name`: directory name containing `.git/`.
/// - `branch`: current HEAD branch, or `"detached"` if detached HEAD, or
/// `None` if branch can't be read.
/// - `commit`: 40-hex commit SHA at HEAD, or `None` if empty repo / read
/// failure.
///
/// `.git/` as a file (worktree marker / submodule) returns `None` for
/// `branch` and `commit` and falls back to the parent dir name for `name`.
pub fn detect_repo(path: &Path) -> Option<RepoMeta> {
let mut cur = if path.is_dir() { path } else { path.parent()? };
loop {
let dotgit = cur.join(".git");
if dotgit.is_dir() {
let name = cur.file_name()?.to_string_lossy().into_owned();
let (branch, commit) = read_head(cur);
return Some(RepoMeta { name, branch, commit });
} else if dotgit.is_file() {
let name = cur.file_name()?.to_string_lossy().into_owned();
return Some(RepoMeta { name, branch: None, commit: None });
}
cur = cur.parent()?;
}
}
fn read_head(repo_dir: &Path) -> (Option<String>, Option<String>) {
match gix::open(repo_dir) {
Ok(repo) => {
let branch = repo
.head_name()
.ok()
.flatten()
.map(|n| n.shorten().to_string())
.or_else(|| Some("detached".to_string()));
let commit = repo
.head_id()
.ok()
.map(|id| id.to_string());
(branch, commit)
}
Err(_) => (None, None),
}
}