Skip to main content

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;