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