posthog_cli/utils/
git.rs

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