Skip to main content

opensession_local_db/
git.rs

1use std::process::Command;
2
3/// Git metadata collected from the working directory at session time.
4#[derive(Debug, Clone, Default)]
5pub struct GitContext {
6    pub remote: Option<String>,
7    pub branch: Option<String>,
8    pub commit: Option<String>,
9    pub repo_name: Option<String>,
10}
11
12/// Extract git context from a working directory.
13/// Returns a default (empty) context if the directory is not inside a git repo.
14pub fn extract_git_context(cwd: &str) -> GitContext {
15    // Check if inside a git repo
16    let toplevel = git_cmd(cwd, &["rev-parse", "--show-toplevel"]);
17    if toplevel.is_none() {
18        return GitContext::default();
19    }
20
21    let remote = git_cmd(cwd, &["remote", "get-url", "origin"]);
22    let branch = git_cmd(cwd, &["rev-parse", "--abbrev-ref", "HEAD"]);
23    let commit = git_cmd(cwd, &["rev-parse", "HEAD"]);
24    let repo_name = remote
25        .as_deref()
26        .and_then(normalize_repo_name)
27        .map(String::from);
28
29    GitContext {
30        remote,
31        branch,
32        commit,
33        repo_name,
34    }
35}
36
37/// Normalize a git remote URL to "owner/repo" form.
38///
39/// Handles:
40///   - `https://github.com/foo/bar.git` → `foo/bar`
41///   - `git@github.com:foo/bar.git` → `foo/bar`
42///   - `ssh://git@github.com/foo/bar` → `foo/bar`
43pub fn normalize_repo_name(remote_url: &str) -> Option<&str> {
44    let s = remote_url.trim();
45
46    // SSH: git@host:owner/repo.git
47    if let Some(rest) = s.strip_prefix("git@") {
48        let path = rest.split_once(':')?.1;
49        let path = path.strip_suffix(".git").unwrap_or(path);
50        return if path.contains('/') { Some(path) } else { None };
51    }
52
53    // HTTPS or SSH scheme: https://host/owner/repo.git  or  ssh://git@host/owner/repo
54    if s.starts_with("https://") || s.starts_with("http://") || s.starts_with("ssh://") {
55        // Find the path after the host
56        let without_scheme = s.split("://").nth(1)?;
57        // skip "git@host/" or "host/"
58        let path_start = without_scheme.find('/')? + 1;
59        let path = &without_scheme[path_start..];
60        let path = path.strip_suffix(".git").unwrap_or(path);
61        return if path.contains('/') { Some(path) } else { None };
62    }
63
64    None
65}
66
67fn git_cmd(cwd: &str, args: &[&str]) -> Option<String> {
68    let output = Command::new("git")
69        .arg("-C")
70        .arg(cwd)
71        .args(args)
72        .output()
73        .ok()?;
74
75    if !output.status.success() {
76        return None;
77    }
78    let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
79    if stdout.is_empty() {
80        None
81    } else {
82        Some(stdout)
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn test_normalize_https() {
92        assert_eq!(
93            normalize_repo_name("https://github.com/hwisu/opensession.git"),
94            Some("hwisu/opensession")
95        );
96    }
97
98    #[test]
99    fn test_normalize_ssh() {
100        assert_eq!(
101            normalize_repo_name("git@github.com:hwisu/opensession.git"),
102            Some("hwisu/opensession")
103        );
104    }
105
106    #[test]
107    fn test_normalize_no_suffix() {
108        assert_eq!(
109            normalize_repo_name("https://github.com/foo/bar"),
110            Some("foo/bar")
111        );
112    }
113
114    #[test]
115    fn test_normalize_invalid() {
116        assert_eq!(normalize_repo_name("not-a-url"), None);
117    }
118}