ito_core/git_remote.rs
1//! Git remote URL resolution for org/repo namespace discovery.
2//!
3//! Provides helpers to determine the `(org, repo)` pair for the current
4//! project by consulting config first and falling back to the `origin` remote
5//! URL when config is incomplete.
6
7use std::path::Path;
8
9use ito_config::types::BackendApiConfig;
10
11use crate::errors::{CoreError, CoreResult};
12use crate::process::{ProcessRequest, ProcessRunner, SystemProcessRunner};
13
14/// Resolve the `(org, repo)` pair for the current project.
15///
16/// Resolution order:
17///
18/// 1. `backend.project.org` / `backend.project.repo` from the supplied config
19/// (both must be non-empty for this source to be used).
20/// 2. Parse the `origin` remote URL via `git remote get-url origin` run in
21/// `repo_root`, delegating URL parsing to
22/// [`ito_common::git_url::parse_remote_url_org_repo`].
23///
24/// Returns `None` when neither source yields a complete pair (e.g., no config
25/// values and no `origin` remote, or the remote URL is in an unrecognised
26/// format).
27pub fn resolve_org_repo_from_config_or_remote(
28 repo_root: &Path,
29 config: &BackendApiConfig,
30) -> Option<(String, String)> {
31 let runner = SystemProcessRunner;
32 resolve_org_repo_from_config_or_remote_with_runner(&runner, repo_root, config)
33}
34
35/// Testable inner implementation that accepts an injected [`ProcessRunner`].
36pub(crate) fn resolve_org_repo_from_config_or_remote_with_runner(
37 runner: &dyn ProcessRunner,
38 repo_root: &Path,
39 config: &BackendApiConfig,
40) -> Option<(String, String)> {
41 // 1. Config takes priority when both fields are present and non-empty.
42 let config_org = config
43 .project
44 .org
45 .as_deref()
46 .filter(|s| !s.is_empty())
47 .map(String::from);
48 let config_repo = config
49 .project
50 .repo
51 .as_deref()
52 .filter(|s| !s.is_empty())
53 .map(String::from);
54
55 if let (Some(org), Some(repo)) = (config_org, config_repo) {
56 return Some((org, repo));
57 }
58
59 // 2. Fall back to parsing the `origin` remote URL.
60 let url = get_origin_remote_url(runner, repo_root)?;
61 ito_common::git_url::parse_remote_url_org_repo(&url)
62}
63
64/// Parse `<org>/<repo>` from a git remote URL.
65///
66/// This is a thin re-export of
67/// [`ito_common::git_url::parse_remote_url_org_repo`] for callers that already
68/// depend on `ito-core` and do not want to add a direct `ito-common` dependency.
69///
70/// See the `ito_common::git_url` module for the full list of supported URL
71/// formats and edge-case behaviour.
72pub fn parse_remote_url_org_repo(url: &str) -> Option<(String, String)> {
73 ito_common::git_url::parse_remote_url_org_repo(url)
74}
75
76/// Attempt to resolve `(org, repo)` from the `origin` remote URL only.
77///
78/// Runs `git remote get-url origin` in `repo_root` and parses the result.
79/// Returns `Ok(Some((org, repo)))` on success, `Ok(None)` when no origin is
80/// configured or the URL format is not recognised, and `Err` only on process
81/// execution failure.
82pub fn resolve_org_repo_from_remote(repo_root: &Path) -> CoreResult<Option<(String, String)>> {
83 let runner = SystemProcessRunner;
84 let request = ProcessRequest::new("git")
85 .args(["remote", "get-url", "origin"])
86 .current_dir(repo_root);
87 let output = runner
88 .run(&request)
89 .map_err(|e| CoreError::process(format!("git remote get-url origin failed: {e}")))?;
90 if !output.success {
91 return Ok(None);
92 }
93 let url = output.stdout.trim().to_string();
94 Ok(ito_common::git_url::parse_remote_url_org_repo(&url))
95}
96
97/// Run `git remote get-url origin` in `repo_root` and return the trimmed URL.
98///
99/// Returns `None` when the command fails or produces no output (e.g., no
100/// `origin` remote is configured).
101fn get_origin_remote_url(runner: &dyn ProcessRunner, repo_root: &Path) -> Option<String> {
102 let request = ProcessRequest::new("git")
103 .args(["remote", "get-url", "origin"])
104 .current_dir(repo_root);
105 let output = runner.run(&request).ok()?;
106 if !output.success {
107 return None;
108 }
109 let url = output.stdout.trim().to_string();
110 if url.is_empty() {
111 return None;
112 }
113 Some(url)
114}
115
116#[cfg(test)]
117#[path = "git_remote_tests.rs"]
118mod git_remote_tests;