1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::io::Read;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct GitInfo {
9 #[serde(skip_serializing_if = "Option::is_none")]
10 pub remote_url: Option<String>,
11 #[serde(skip_serializing_if = "Option::is_none")]
12 pub repo_name: Option<String>,
13 pub branch: String,
14 pub commit_id: String,
15}
16
17pub fn get_git_info(dir: Option<PathBuf>) -> Result<Option<GitInfo>> {
18 let git_dir = match find_git_dir(dir) {
19 Some(dir) => dir,
20 None => return Ok(None),
21 };
22
23 let remote_url = get_remote_url(&git_dir);
24 let repo_name = get_repo_name(&git_dir);
25 let branch = get_current_branch(&git_dir).context("Failed to determine current branch")?;
26 let commit = get_head_commit(&git_dir, &branch).context("Failed to determine commit ID")?;
27
28 Ok(Some(GitInfo {
29 remote_url,
30 repo_name,
31 branch,
32 commit_id: commit,
33 }))
34}
35
36fn find_git_dir(dir: Option<PathBuf>) -> Option<PathBuf> {
37 let mut current_dir = dir.unwrap_or(std::env::current_dir().ok()?);
38
39 loop {
40 let git_dir = current_dir.join(".git");
41 if git_dir.is_dir() {
42 return Some(git_dir);
43 }
44
45 if !current_dir.pop() {
46 return None;
47 }
48 }
49}
50
51pub fn get_remote_url(git_dir: &Path) -> Option<String> {
52 let config_path = git_dir.join("config");
54 if config_path.exists() {
55 let config_content = match fs::read_to_string(&config_path) {
56 Ok(content) => content,
57 Err(_) => return None,
58 };
59
60 for line in config_content.lines() {
61 let line = line.trim();
62 if line.starts_with("url = ") {
63 let url = line.trim_start_matches("url = ").trim();
64 let normalized = if url.ends_with(".git") {
65 url.to_string()
66 } else {
67 format!("{url}.git")
68 };
69 return Some(normalized);
70 }
71 }
72 }
73
74 None
75}
76
77pub fn get_repo_name(git_dir: &Path) -> Option<String> {
78 let config_path = git_dir.join("config");
80 if config_path.exists() {
81 let config_content = match fs::read_to_string(&config_path) {
82 Ok(content) => content,
83 Err(_) => return None,
84 };
85
86 for line in config_content.lines() {
87 let line = line.trim();
88 if line.starts_with("url = ") {
89 let url = line.trim_start_matches("url = ");
90 if let Some(repo_name) = url.split('/').next_back() {
91 let clean_name = repo_name.trim_end_matches(".git");
92 return Some(clean_name.to_string());
93 }
94 }
95 }
96 }
97
98 if let Some(parent) = git_dir.parent() {
99 if let Some(name) = parent.file_name() {
100 return Some(name.to_string_lossy().to_string());
101 }
102 }
103
104 None
105}
106
107fn get_current_branch(git_dir: &Path) -> Result<String> {
108 let head_path = git_dir.join("HEAD");
110 let mut head_content = String::new();
111 fs::File::open(&head_path)
112 .with_context(|| format!("Failed to open HEAD file at {head_path:?}"))?
113 .read_to_string(&mut head_content)
114 .context("Failed to read HEAD file")?;
115
116 if head_content.starts_with("ref: refs/heads/") {
118 Ok(head_content
119 .trim_start_matches("ref: refs/heads/")
120 .trim()
121 .to_string())
122 } else if head_content.trim().len() == 40 || head_content.trim().len() == 64 {
123 Ok("HEAD-detached".to_string())
124 } else {
125 anyhow::bail!("Unrecognized HEAD format")
126 }
127}
128
129fn get_head_commit(git_dir: &Path, branch: &str) -> Result<String> {
130 if branch == "HEAD-detached" {
131 let head_path = git_dir.join("HEAD");
133 let mut head_content = String::new();
134 fs::File::open(&head_path)
135 .with_context(|| format!("Failed to open HEAD file at {head_path:?}"))?
136 .read_to_string(&mut head_content)
137 .context("Failed to read HEAD file")?;
138
139 return Ok(head_content.trim().to_string());
140 }
141
142 let ref_path = git_dir.join("refs/heads").join(branch);
144 if ref_path.exists() {
145 let mut commit_id = String::new();
146 fs::File::open(&ref_path)
147 .with_context(|| format!("Failed to open branch reference at {ref_path:?}"))?
148 .read_to_string(&mut commit_id)
149 .context("Failed to read branch reference file")?;
150
151 return Ok(commit_id.trim().to_string());
152 }
153
154 anyhow::bail!("Could not determine commit ID")
155}