Skip to main content

omni_dev/git/
remote.rs

1//! Git remote operations.
2
3use anyhow::{Context, Result};
4use git2::{BranchType, Repository};
5use serde::{Deserialize, Serialize};
6
7/// Remote repository information.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct RemoteInfo {
10    /// Name of the remote (e.g., "origin", "upstream").
11    pub name: String,
12    /// URI of the remote repository.
13    pub uri: String,
14    /// Detected main branch name for this remote.
15    pub main_branch: String,
16}
17
18impl RemoteInfo {
19    /// Returns all remotes for a repository.
20    pub fn get_all_remotes(repo: &Repository) -> Result<Vec<Self>> {
21        let mut remotes = Vec::new();
22        let remote_names = repo.remotes().context("Failed to get remote names")?;
23
24        for name in remote_names.iter().flatten() {
25            if let Ok(remote) = repo.find_remote(name) {
26                let uri = remote.url().unwrap_or("").to_string();
27                let main_branch = Self::detect_main_branch(repo, name)?;
28
29                remotes.push(RemoteInfo {
30                    name: name.to_string(),
31                    uri,
32                    main_branch,
33                });
34            }
35        }
36
37        Ok(remotes)
38    }
39
40    /// Detects the main branch for a remote.
41    fn detect_main_branch(repo: &Repository, remote_name: &str) -> Result<String> {
42        // First try to get the remote HEAD reference
43        let head_ref_name = format!("refs/remotes/{}/HEAD", remote_name);
44        if let Ok(head_ref) = repo.find_reference(&head_ref_name) {
45            if let Some(target) = head_ref.symbolic_target() {
46                // Extract branch name from refs/remotes/origin/main
47                if let Some(branch_name) =
48                    target.strip_prefix(&format!("refs/remotes/{}/", remote_name))
49                {
50                    return Ok(branch_name.to_string());
51                }
52            }
53        }
54
55        // Try using GitHub CLI for GitHub repositories
56        if let Ok(remote) = repo.find_remote(remote_name) {
57            if let Some(uri) = remote.url() {
58                if uri.contains("github.com") {
59                    if let Ok(main_branch) = Self::get_github_default_branch(uri) {
60                        return Ok(main_branch);
61                    }
62                }
63            }
64        }
65
66        // Fallback to checking common branch names, preferring origin remote
67        let common_branches = ["main", "master", "develop"];
68
69        // First, check if this is the origin remote or if origin remote branches exist
70        if remote_name == "origin" {
71            for branch_name in &common_branches {
72                let reference_name = format!("refs/remotes/origin/{}", branch_name);
73                if repo.find_reference(&reference_name).is_ok() {
74                    return Ok(branch_name.to_string());
75                }
76            }
77        } else {
78            // For non-origin remotes, first check if origin has these branches
79            for branch_name in &common_branches {
80                let origin_reference = format!("refs/remotes/origin/{}", branch_name);
81                if repo.find_reference(&origin_reference).is_ok() {
82                    return Ok(branch_name.to_string());
83                }
84            }
85
86            // Then check the actual remote
87            for branch_name in &common_branches {
88                let reference_name = format!("refs/remotes/{}/{}", remote_name, branch_name);
89                if repo.find_reference(&reference_name).is_ok() {
90                    return Ok(branch_name.to_string());
91                }
92            }
93        }
94
95        // If no common branch found, try to find any branch
96        let branch_iter = repo.branches(Some(BranchType::Remote))?;
97        for branch_result in branch_iter {
98            let (branch, _) = branch_result?;
99            if let Some(name) = branch.name()? {
100                if name.starts_with(&format!("{}/", remote_name)) {
101                    let branch_name = name
102                        .strip_prefix(&format!("{}/", remote_name))
103                        .unwrap_or(name);
104                    return Ok(branch_name.to_string());
105                }
106            }
107        }
108
109        // If still no branch found, return "unknown"
110        Ok("unknown".to_string())
111    }
112
113    /// Returns the default branch from GitHub using gh CLI.
114    fn get_github_default_branch(uri: &str) -> Result<String> {
115        use std::process::Command;
116
117        // Extract repository name from URI
118        let repo_name = Self::extract_github_repo_name(uri)?;
119
120        // Use gh CLI to get default branch
121        let output = Command::new("gh")
122            .args([
123                "repo",
124                "view",
125                &repo_name,
126                "--json",
127                "defaultBranchRef",
128                "--jq",
129                ".defaultBranchRef.name",
130            ])
131            .output();
132
133        match output {
134            Ok(output) if output.status.success() => {
135                let branch_name = String::from_utf8_lossy(&output.stdout).trim().to_string();
136                if !branch_name.is_empty() && branch_name != "null" {
137                    Ok(branch_name)
138                } else {
139                    anyhow::bail!("GitHub CLI returned empty or null branch name")
140                }
141            }
142            _ => anyhow::bail!("Failed to get default branch from GitHub CLI"),
143        }
144    }
145
146    /// Extracts GitHub repository name from URI.
147    fn extract_github_repo_name(uri: &str) -> Result<String> {
148        // Handle both SSH and HTTPS GitHub URIs
149        let repo_name = if uri.starts_with("git@github.com:") {
150            // SSH format: git@github.com:owner/repo.git
151            uri.strip_prefix("git@github.com:")
152                .and_then(|s| s.strip_suffix(".git"))
153                .unwrap_or(uri.strip_prefix("git@github.com:").unwrap_or(uri))
154        } else if uri.contains("github.com") {
155            // HTTPS format: https://github.com/owner/repo.git
156            uri.split("github.com/")
157                .nth(1)
158                .and_then(|s| s.strip_suffix(".git"))
159                .unwrap_or(uri.split("github.com/").nth(1).unwrap_or(uri))
160        } else {
161            anyhow::bail!("Not a GitHub URI: {}", uri);
162        };
163
164        if repo_name.split('/').count() != 2 {
165            anyhow::bail!("Invalid GitHub repository format: {}", repo_name);
166        }
167
168        Ok(repo_name.to_string())
169    }
170}