use std::path::{Path, PathBuf};
use crate::inline::GitHubContext;
pub fn detect_github_context(start_path: &Path) -> Option<GitHubContext> {
let parent = start_path.parent().filter(|p| !p.as_os_str().is_empty());
let start_dir = if start_path.is_dir() {
start_path
} else {
parent.unwrap_or_else(|| Path::new("."))
};
let repo = gix::discover(start_dir).ok()?;
let remote = repo.find_remote("origin").ok()?;
let url = remote.url(gix::remote::Direction::Fetch)?;
parse_github_url(url.to_bstring().to_string().as_str())
}
fn parse_github_url(url: &str) -> Option<GitHubContext> {
let repo_path = if let Some(rest) = url.strip_prefix("git@github.com:") {
rest
} else if url.contains("github.com") {
url.strip_prefix("https://")
.or_else(|| url.strip_prefix("http://"))?
.strip_prefix("github.com/")?
} else {
return None;
};
let repo_path = repo_path.strip_suffix(".git").unwrap_or(repo_path);
let (owner, repo) = repo_path.split_once('/')?;
let repo = repo.split('/').next()?;
Some(GitHubContext {
owner: owner.to_string(),
repo: repo.to_string(),
})
}
fn discover_repo(path: &Path) -> Option<(PathBuf, gix::Repository)> {
let abs = path.canonicalize().ok()?;
let repo = gix::discover(abs.parent()?).ok()?;
Some((abs, repo))
}
pub fn head_blob_text(path: &Path) -> Option<String> {
let (abs, repo) = discover_repo(path)?;
let workdir = repo.workdir()?.canonicalize().ok()?;
let rel = abs.strip_prefix(&workdir).ok()?;
let tree = repo.head_commit().ok()?.tree().ok()?;
let entry = tree.lookup_entry_by_path(rel).ok()??;
let mut blob = entry.object().ok()?;
String::from_utf8(std::mem::take(&mut blob.data)).ok()
}
pub fn git_watch_dirs(path: &Path) -> Option<(PathBuf, PathBuf)> {
let (_, repo) = discover_repo(path)?;
Some((
repo.git_dir().to_path_buf(),
repo.common_dir().to_path_buf(),
))
}
pub fn is_git_base_path(p: &Path) -> bool {
if p.extension().is_some_and(|e| e == "lock") {
return false;
}
let mut saw_refs = false;
for c in p.components() {
match c.as_os_str().to_str() {
Some("refs") => saw_refs = true,
Some("logs") if !saw_refs => return false,
_ => {}
}
}
let name = p.file_name().and_then(|n| n.to_str());
if matches!(name, Some("HEAD" | "packed-refs")) {
return true;
}
p.components().any(|c| c.as_os_str() == "refs")
}
pub fn parse_github_repo_string(s: &str) -> Option<GitHubContext> {
let (owner, repo) = s.split_once('/')?;
if owner.is_empty() || repo.is_empty() {
return None;
}
Some(GitHubContext {
owner: owner.to_string(),
repo: repo.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_github_ssh_url() {
let ctx = parse_github_url("git@github.com:wilfred/writ.git").unwrap();
assert_eq!(ctx.owner, "wilfred");
assert_eq!(ctx.repo, "writ");
let ctx = parse_github_url("git@github.com:wilfred/writ").unwrap();
assert_eq!(ctx.owner, "wilfred");
assert_eq!(ctx.repo, "writ");
let ctx = parse_github_url("git@github.com:wilfred/writ/extra").unwrap();
assert_eq!(ctx.owner, "wilfred");
assert_eq!(ctx.repo, "writ");
}
#[test]
fn detect_context_from_file_matches_dir() {
let from_file = detect_github_context(Path::new("src/git.rs"));
let from_dir = detect_github_context(Path::new("src"));
assert_eq!(from_file, from_dir);
}
#[test]
fn test_parse_github_https_url() {
let ctx = parse_github_url("https://github.com/wilfred/writ.git").unwrap();
assert_eq!(ctx.owner, "wilfred");
assert_eq!(ctx.repo, "writ");
let ctx = parse_github_url("https://github.com/wilfred/writ").unwrap();
assert_eq!(ctx.owner, "wilfred");
assert_eq!(ctx.repo, "writ");
}
#[test]
fn test_parse_non_github_url() {
assert!(parse_github_url("git@gitlab.com:owner/repo.git").is_none());
assert!(parse_github_url("https://gitlab.com/owner/repo").is_none());
}
#[test]
fn test_parse_github_repo_string() {
let ctx = parse_github_repo_string("wilfred/writ").unwrap();
assert_eq!(ctx.owner, "wilfred");
assert_eq!(ctx.repo, "writ");
assert!(parse_github_repo_string("invalid").is_none());
assert!(parse_github_repo_string("/repo").is_none());
assert!(parse_github_repo_string("owner/").is_none());
}
#[test]
fn test_detect_github_context_in_repo() {
let ctx = detect_github_context(std::path::Path::new(".")).unwrap();
assert_eq!(ctx.owner, "wilfreddenton");
assert_eq!(ctx.repo, "writ");
}
}
#[cfg(test)]
mod head_blob_tests {
use super::*;
#[test]
fn head_blob_reads_tracked_file() {
let text = head_blob_text(std::path::Path::new("Cargo.toml")).unwrap();
assert!(text.contains("[package]"));
}
#[test]
fn head_blob_none_for_missing() {
assert!(head_blob_text(std::path::Path::new("does/not/exist.md")).is_none());
}
#[test]
fn git_watch_dirs_resolves_repo() {
let (git_dir, common_dir) = git_watch_dirs(Path::new("Cargo.toml")).unwrap();
assert!(git_dir.ends_with(".git"), "git_dir: {git_dir:?}");
assert_eq!(git_dir, common_dir);
assert!(common_dir.join("HEAD").exists());
}
#[test]
fn git_watch_dirs_none_outside_repo() {
assert!(git_watch_dirs(Path::new("/does/not/exist.md")).is_none());
}
#[test]
fn is_git_base_path_classifies() {
assert!(is_git_base_path(Path::new("/repo/.git/HEAD")));
assert!(is_git_base_path(Path::new("/repo/.git/packed-refs")));
assert!(is_git_base_path(Path::new("/repo/.git/refs/heads/main")));
assert!(!is_git_base_path(Path::new("/repo/.git/index")));
assert!(!is_git_base_path(Path::new("/repo/.git/HEAD.lock")));
assert!(!is_git_base_path(Path::new(
"/repo/.git/refs/heads/main.lock"
)));
assert!(!is_git_base_path(Path::new("/repo/.git/logs/HEAD")));
assert!(!is_git_base_path(Path::new(
"/repo/.git/logs/refs/heads/main"
)));
assert!(!is_git_base_path(Path::new("/repo/.git/objects/ab/cdef")));
assert!(is_git_base_path(Path::new("/repo/.git/refs/heads/logs")));
}
}