use std::{collections::HashMap, path::PathBuf};
use eros::{Context, bail};
use regex::Regex;
use tokio::{fs, process::Command};
use tracing::trace;
use crate::build::{ModuleData, RemoteModuleInfo, SourceInfoKind, read_module_file};
use super::{GitProvider, ModuleFileData, ReferenceInfo, path_in_cache_dir};
#[derive(Debug)]
pub struct Git {
provider: String,
repo_owner: String,
repo_name: String,
url: String,
commit: String,
}
impl Git {
pub fn new(url: String, commit: String) -> eros::Result<Self> {
let RepoInfo {
provider,
owner,
name,
} = url_to_repo_info(&url)?;
Ok(Git {
provider,
repo_owner: owner,
repo_name: name,
url,
commit,
})
}
}
impl GitProvider for Git {
async fn retrieve_module(
&self,
name_to_path: HashMap<String, String>,
) -> eros::Result<HashMap<String, ModuleFileData>> {
let mut module_to_files: HashMap<String, ModuleFileData> = HashMap::new();
for (name, module_path) in name_to_path.into_iter() {
let module_path_cache = path_in_cache_dir(
&PathBuf::from(&module_path),
&self.provider,
&self.repo_owner,
&self.repo_name,
&self.commit,
);
if !module_path_cache.exists() {
trace!(
"Module `{}` not found in cache. Retrieving from remote...",
name
);
self.retrieve_file_and_put_at(&module_path, &module_path_cache)
.await?;
}
assert!(module_path_cache.exists());
let module_data: ModuleData = read_module_file(&module_path_cache).await?;
let source_info = SourceInfoKind::Remote(RemoteModuleInfo {
url: self.url.clone(),
repo_owner: self.repo_owner.clone(),
repo_name: self.repo_name.clone(),
commit: self.commit.clone(),
path: module_path.clone(),
name: name.clone(),
});
module_to_files.insert(
name,
ModuleFileData {
containerfile_data: module_data.containerfile,
config_data: module_data.config,
install_stage_data: module_data.install_stage,
source_info,
},
);
}
Ok(module_to_files)
}
fn reference_info<'a>(&'a self) -> ReferenceInfo<'a> {
ReferenceInfo {
provider: self.provider.as_str(),
repo_owner: self.repo_owner.as_str(),
repo_name: self.repo_name.as_str(),
url: self.url.as_str(),
commit: self.commit.as_str(),
}
}
async fn extract_remote_path_data(&self, remote_path: &str) -> eros::Result<String> {
let provider_git_cache_dir = dirs::cache_dir()
.expect("Could not determine cache directory of platform")
.join("containeryard")
.join("sources")
.join("git_repos")
.join(&self.provider)
.join(&self.repo_owner);
let repo_dir = provider_git_cache_dir.join(&self.repo_name);
let mut will_clone = false;
if repo_dir.is_dir() {
if !repo_dir.join(".git").is_dir() {
bail!(
"Cached directory for repo `{}` exists at `{}`, but it is not a git directory.",
self.url,
repo_dir.to_str().unwrap_or("")
)
}
trace!("Found a git cloned repo for `{}`", self.url,);
} else {
will_clone = true;
fs::create_dir_all(&repo_dir).await?;
}
if will_clone {
trace!(
"Cloning git repo `{}` to `{}`",
self.url,
provider_git_cache_dir.to_str().unwrap_or("")
);
let clone_output = Command::new("git")
.args(["clone", &self.url])
.current_dir(&provider_git_cache_dir)
.output()
.await
.map_err(|e| {
eros::error!(
"Failed to execute git command to clone {}:\n{}",
self.url,
e
)
})?;
if !clone_output.status.success() {
bail!(
"Git failed with {}.\nCould not clone git repo `{}` to `{}`.\nstdout:\n{}\nstderr:\n{}",
&clone_output.status,
self.url,
provider_git_cache_dir.to_str().unwrap_or(""),
String::from_utf8_lossy(&clone_output.stdout),
String::from_utf8_lossy(&clone_output.stderr)
);
}
} else {
trace!(
"Pulling git repo `{}` to `{}`",
self.url,
provider_git_cache_dir.to_str().unwrap_or("")
);
let fetch_output = Command::new("git")
.args(["fetch", "--all", "--prune"])
.current_dir(&repo_dir)
.output()
.await
.map_err(|e| {
eros::error!(
"Failed to execute git command to pull the latest for {}:\n{}",
self.url,
e
)
})?;
if !fetch_output.status.success() {
bail!(
"Git failed with {}.\nCould not pull git repo `{}` to `{}`.\nstdout:\n{}\nstderr:\n{}",
&fetch_output.status,
self.url,
provider_git_cache_dir.to_str().unwrap_or(""),
String::from_utf8_lossy(&fetch_output.stdout),
String::from_utf8_lossy(&fetch_output.stderr)
);
}
}
trace!(
"Checking out commit `{}` in repo `{}`",
self.commit, self.url
);
let checkout_output = Command::new("git")
.args(["checkout", &self.commit])
.current_dir(&repo_dir)
.output()
.await
.map_err(|e| {
eros::error!(
"Failed to execute git command to checkout {}:\n{}",
self.url,
e
)
})?;
if !checkout_output.status.success() {
bail!(
"Git failed with {}.\nCould not checkout commit `{}` in git repo `{}`.\nstdout:\n{}\nstderr:\n{}",
&checkout_output.status,
self.commit,
self.url,
String::from_utf8_lossy(&checkout_output.stdout),
String::from_utf8_lossy(&checkout_output.stderr)
);
}
let remote_file_path = repo_dir.join(remote_path);
if !remote_file_path.is_file() {
bail!(
"Could not find file at remote path `{}` in repo `{}` at commit `{}`",
&remote_path,
&self.url,
&self.commit
)
}
let file_data = fs::read_to_string(&remote_file_path)
.await
.map_err(|e| eros::error!(e))
.with_context(|| format!("Could not read `{}`", &remote_file_path.display()))?;
Ok(file_data)
}
}
struct RepoInfo {
provider: String,
owner: String,
name: String,
}
fn url_to_repo_info(url: &str) -> eros::Result<RepoInfo> {
let owner;
let name;
if url.starts_with("git@") {
(owner, name) = extract_user_and_repo_from_ssh(url)?
} else if url.starts_with("http") {
(owner, name) = extract_user_and_repo_from_http(url)?;
} else {
bail!("Unknown url type for `{url}`. Expected to start with `git@` or `http`")
}
let provider = if url.contains("github.com") {
"github".to_string()
} else {
"unknown".to_string()
};
Ok(RepoInfo {
provider,
owner,
name,
})
}
fn extract_user_and_repo_from_ssh(ssh_url: &str) -> eros::Result<(String, String)> {
let re = Regex::new(r"^[\w-]+@[\w.-]+:([\w-]+)/([\w-]+)(?:\.git)?$").unwrap();
re.captures(ssh_url)
.and_then(|caps| {
let user = caps.get(1).map(|m| m.as_str().to_string())?;
let repo = caps.get(2).map(|m| m.as_str().to_string())?;
Some((user, repo))
})
.ok_or(eros::error!(
"Could not extract user and repo from ssh url `{}`",
ssh_url
))
}
fn extract_user_and_repo_from_http(url: &str) -> eros::Result<(String, String)> {
let re = Regex::new(r"^https?://[\w.-]+/([\w-]+)/([\w-]+)(?:\.git)?$").unwrap();
re.captures(url)
.and_then(|caps| {
let user = caps.get(1).map(|m| m.as_str().to_string())?;
let repo = caps.get(2).map(|m| m.as_str().to_string())?;
Some((user, repo))
})
.ok_or(eros::error!(
"Could not extract user and repo from url `{}`",
url
))
}