pub mod github;
pub mod gitlab;
mod info;
pub use github::GitHubProvider;
pub use gitlab::GitLabProvider;
pub use info::{PlatformData, RemoteRefInfo};
use std::io::ErrorKind;
use std::path::Path;
use std::process::Output;
use anyhow::bail;
use crate::git::error::GitError;
use crate::git::{RefType, Repository};
use crate::shell_exec::Cmd;
pub trait RemoteRefProvider {
fn ref_type(&self) -> RefType;
fn fetch_info(&self, number: u32, repo: &Repository) -> anyhow::Result<RemoteRefInfo>;
fn ref_path(&self, number: u32) -> String;
fn tracking_ref(&self, number: u32) -> String {
format!("refs/{}", self.ref_path(number))
}
}
pub(super) struct CliApiRequest<'a> {
pub tool: &'a str,
pub args: &'a [&'a str],
pub repo_root: &'a Path,
pub prompt_env: (&'a str, &'a str),
pub install_hint: &'a str,
pub run_context: &'a str,
}
pub(super) fn run_cli_api(request: CliApiRequest<'_>) -> anyhow::Result<Output> {
match Cmd::new(request.tool)
.args(request.args.iter().copied())
.current_dir(request.repo_root)
.env(request.prompt_env.0, request.prompt_env.1)
.run()
{
Ok(output) => Ok(output),
Err(error) => {
if error.kind() == ErrorKind::NotFound {
bail!("{}", request.install_hint);
}
Err(anyhow::Error::from(error).context(request.run_context.to_string()))
}
}
}
pub(super) fn cli_api_error_details(output: &Output) -> String {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.trim().is_empty() {
String::from_utf8_lossy(&output.stdout).trim().to_string()
} else {
stderr.trim().to_string()
}
}
pub(super) fn cli_api_error(ref_type: RefType, message: String, output: &Output) -> anyhow::Error {
GitError::CliApiError {
ref_type,
message,
stderr: cli_api_error_details(output),
}
.into()
}
pub(super) fn cli_config_value(tool: &str, key: &str) -> Option<String> {
Cmd::new(tool)
.args(["config", "get", key])
.run()
.ok()
.filter(|output| output.status.success())
.map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string())
}
pub fn find_remote(repo: &Repository, info: &RemoteRefInfo) -> Result<String, GitError> {
let (owner, repo_name) = match &info.platform_data {
PlatformData::GitHub {
base_owner,
base_repo,
..
}
| PlatformData::GitLab {
base_owner,
base_repo,
..
} => (base_owner.as_str(), base_repo.as_str()),
};
repo.find_remote_for_repo(None, owner, repo_name)
.ok_or_else(|| {
let suggested_url = match &info.platform_data {
PlatformData::GitHub {
host,
base_owner,
base_repo,
..
} => github::fork_remote_url(host, base_owner, base_repo),
PlatformData::GitLab {
host,
base_owner,
base_repo,
..
} => gitlab::fork_remote_url(host, base_owner, base_repo),
};
GitError::NoRemoteForRepo {
owner: owner.to_string(),
repo: repo_name.to_string(),
suggested_url,
}
})
}
pub fn branch_tracks_ref(
repo_root: &Path,
branch: &str,
provider: &dyn RemoteRefProvider,
number: u32,
expected_remote: Option<&str>,
) -> Option<bool> {
let expected_ref = provider.tracking_ref(number);
crate::git::branch_tracks_ref(repo_root, branch, &expected_ref, expected_remote)
}
pub fn local_branch_name(info: &RemoteRefInfo) -> String {
info.source_branch.clone()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ref_paths() {
let gh = GitHubProvider;
assert_eq!(gh.ref_path(123), "pull/123/head");
assert_eq!(gh.tracking_ref(123), "refs/pull/123/head");
let gl = GitLabProvider;
assert_eq!(gl.ref_path(42), "merge-requests/42/head");
assert_eq!(gl.tracking_ref(42), "refs/merge-requests/42/head");
}
}