binocular/search/sources/git/
scope.rs1use anyhow::{bail, Context};
2use std::path::{Path, PathBuf};
3use std::process::Command;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum GitSearchMode {
7 History { file: PathBuf },
8 Branches,
9 Commits,
10}
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct GitSearchScope {
14 pub repo_root: PathBuf,
15 pub mode: GitSearchMode,
16 pub display_path: Option<String>,
17}
18
19pub fn resolve_history_scope(path: &Path) -> anyhow::Result<GitSearchScope> {
20 let path = if path.as_os_str().is_empty() {
21 PathBuf::from(".")
22 } else {
23 path.to_path_buf()
24 };
25
26 let repo_root = git_repo_root(&path)?;
27 let file_path = if path.is_absolute() {
28 path.clone()
29 } else {
30 repo_root.join(&path)
31 };
32 let relative = file_path
33 .strip_prefix(&repo_root)
34 .unwrap_or(&file_path)
35 .to_path_buf();
36 let display_path = relative.to_string_lossy().replace('\\', "/");
37
38 Ok(GitSearchScope {
39 repo_root,
40 mode: GitSearchMode::History { file: relative },
41 display_path: Some(display_path),
42 })
43}
44
45pub fn resolve_repo_scope(
46 start: Option<&Path>,
47 mode: GitSearchMode,
48) -> anyhow::Result<GitSearchScope> {
49 let start = start.unwrap_or_else(|| Path::new("."));
50 let repo_root = git_repo_root(start)?;
51 Ok(GitSearchScope {
52 repo_root,
53 mode,
54 display_path: None,
55 })
56}
57
58pub(crate) fn git_repo_root(path: &Path) -> anyhow::Result<PathBuf> {
59 let cwd = repo_lookup_dir(path);
60 let output = Command::new("git")
61 .arg("rev-parse")
62 .arg("--show-toplevel")
63 .current_dir(cwd)
64 .output()
65 .context("failed to determine git repository root")?;
66
67 if !output.status.success() {
68 let stderr = String::from_utf8_lossy(&output.stderr);
69 bail!(
70 "git rev-parse --show-toplevel failed: {}",
71 if_empty_then(stderr.trim(), "not a git repository")
72 );
73 }
74
75 Ok(PathBuf::from(
76 String::from_utf8_lossy(&output.stdout).trim(),
77 ))
78}
79
80fn repo_lookup_dir(path: &Path) -> PathBuf {
81 if path.is_dir() {
82 return path.to_path_buf();
83 }
84
85 match path.parent() {
86 Some(parent) if !parent.as_os_str().is_empty() => parent.to_path_buf(),
87 _ => PathBuf::from("."),
88 }
89}
90
91pub(crate) fn if_empty_then<'a>(value: &'a str, fallback: &'a str) -> &'a str {
92 if value.is_empty() {
93 fallback
94 } else {
95 value
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn bare_filename_repo_lookup_uses_current_directory() {
105 assert_eq!(
106 repo_lookup_dir(Path::new("Architecture.md")),
107 PathBuf::from(".")
108 );
109 assert_eq!(
110 repo_lookup_dir(Path::new("./Architecture.md")),
111 PathBuf::from(".")
112 );
113 }
114}