use git2::build::RepoBuilder;
use git2::{Cred, Error, ErrorClass, ErrorCode, FetchOptions, RemoteCallbacks, Repository};
use reqwest::blocking::Client;
use serde_json::Value;
use std::collections::HashMap;
use std::{fs, io, path::Display, path::Path, time::Duration, time::Instant};
const API_BASE_URL: &str = "https://api.github.com";
pub struct Scope {
pub name: String,
pub endpoint: String,
pub query: (String, String),
}
pub struct LocalRepo {
name: String,
ssh_url: String,
path: Box<Path>,
}
impl LocalRepo {
pub fn new(name: String, ssh_url: String, base_dir: &Path) -> LocalRepo {
let path = base_dir.join(&name).into_boxed_path();
LocalRepo {
name,
ssh_url,
path,
}
}
pub fn clone(&self, builder: &mut RepoBuilder) -> Result<Duration, Error> {
let start = Instant::now();
builder.clone(&self.ssh_url, &self.path)?;
Result::Ok(start.elapsed())
}
pub fn fetch(&self, options: &mut FetchOptions) -> Result<Duration, Error> {
let start = Instant::now();
let repo = self.open_bare()?;
let mut origin = repo.find_remote("origin")?;
let head = repo.head()?;
if !head.is_branch() {
return Err(Error::new(
ErrorCode::NotFound,
ErrorClass::Reference,
"HEAD does not refer to a branch",
));
}
let branch = match head.shorthand() {
Some(b) => b,
None => {
return Err(Error::new(
ErrorCode::NotFound,
ErrorClass::Reference,
"unable to get branch for HEAD",
))
}
};
origin.fetch(&[branch], Some(options), None)?;
Result::Ok(start.elapsed())
}
pub fn name(&self) -> &str {
&self.name
}
pub fn display_path(&self) -> Display {
self.path.display()
}
pub fn existing_dir(&self) -> bool {
self.path.exists() && self.path.is_dir()
}
pub fn open_bare(&self) -> Result<Repository, Error> {
Repository::open_bare(&self.path)
}
pub fn annihilate(&self) -> io::Result<()> {
fs::remove_dir_all(&self.path)
}
}
pub fn create_callbacks(keyfile: &Path) -> RemoteCallbacks {
let mut callbacks = RemoteCallbacks::new();
callbacks.credentials(|url, username, _| {
match username {
Some(u) => Cred::ssh_key(u, None, keyfile, None ),
None => Err(Error::new(
ErrorCode::User,
ErrorClass::Invalid,
format!("cannot determine username from URL {url}"),
)),
}
});
callbacks
}
pub fn prepare_clone_dir(path: &Path) -> Result<Box<&Path>, String> {
match path.exists() {
true => match path.is_dir() {
true => Ok(Box::new(path)),
false => Err("path exists, but is not a directory".into()),
},
false => match fs::create_dir_all(path) {
Ok(_) => Ok(Box::new(path)),
Err(e) => Err(e.to_string()),
},
}
}
pub fn fetch_repo_ssh_urls_by_name(github_token: String, scope: Scope) -> HashMap<String, String> {
let mut ssh_urls: HashMap<String, String> = HashMap::new();
let client = Client::new();
let url = format!("{API_BASE_URL}/{}", scope.endpoint);
let mut page = 1;
loop {
let req = client
.get(url.clone())
.bearer_auth(&github_token)
.header("Accept", "application/json")
.header("User-Agent", "reqwest")
.query(&[
("per_page", "20"),
("page", &page.to_string()),
(&scope.query.0, &scope.query.1),
]);
match req.send() {
Ok(res) => {
if let Ok(Value::Array(arr)) = res.json::<serde_json::Value>() {
if arr.is_empty() {
break;
}
for repo in arr {
match (repo.get("name"), repo.get("ssh_url")) {
(Some(Value::String(name)), Some(Value::String(ssh_url))) => {
ssh_urls.insert(name.into(), ssh_url.into());
}
_ => eprintln!("skipping repo (missing name/ssh_url)"),
}
}
}
}
Err(e) => eprintln!("request failed: {:?}", e),
}
page += 1;
}
ssh_urls
}