use cargo_auto_lib as cl;
use cl::CargoTomlPublicApiMethods;
use cl::ShellCommandLimitedDoubleQuotesSanitizerTrait;
use cl::BLUE;
use cl::RED;
use cl::RESET;
use cl::YELLOW;
pub trait SendToGitHubApi {
fn send_to_github_api(&self, req: reqwest::blocking::RequestBuilder) -> serde_json::Value;
fn upload_to_github(&self, req: reqwest::RequestBuilder) -> impl std::future::Future<Output = serde_json::Value> + Send;
}
pub fn git_has_remote() -> bool {
let output = std::process::Command::new("git").arg("remote").output().unwrap();
String::from_utf8(output.stdout).unwrap() != ""
}
pub fn git_has_upstream() -> bool {
let output = std::process::Command::new("git").arg("branch").arg("-vv").output().unwrap();
String::from_utf8(output.stdout).unwrap().contains("[")
}
pub fn new_remote_github_repository(github_client: &impl SendToGitHubApi) -> Option<()> {
let cargo_toml = cl::CargoToml::read();
let package_name = cargo_toml.package_name();
let github_owner_or_organization = cargo_toml
.github_owner()
.unwrap_or_else(|| panic!("{RED}ERROR: Element Repository in Cargo.toml does not contain the github_owner!{RESET}"));
if github_owner_or_organization == "github_owner" {
panic!("{RED}Error: The placeholder 'github_owner' in Cargo.toml/repository is not changed to the real github_owner or GitHub Organization.{RESET}")
}
let json_value = github_client.send_to_github_api(github_api_get_authenticated_user());
let Some(authenticated_user_login) = json_value.get("login") else {
panic!("{RED}ERROR: Unrecognized Authenticated on GitHub from secret_token.{RESET}");
};
let authenticated_user_login = authenticated_user_login.as_str().unwrap();
if github_owner_or_organization == authenticated_user_login {
} else {
let json_value = github_client.send_to_github_api(github_api_get_organization(&github_owner_or_organization));
let Some(_organization_login) = json_value.get("login") else {
panic!("{RED}ERROR: Unrecognized Organization on GitHub: {github_owner_or_organization}.{RESET}");
};
}
if !git_has_remote() {
let description = cargo_toml
.package_description()
.unwrap_or_else(|| panic!("{RED}ERROR: Element Description in Cargo.toml does not exist!{RESET}"));
println!(" {BLUE}This project does not have a remote GitHub repository.{RESET}");
let answer = inquire::Text::new(&format!("{BLUE}Do you want to create a new remote GitHub repository? (y/n){RESET}"))
.prompt()
.unwrap();
if answer.to_lowercase() != "y" {
return None;
}
let json_value = if github_owner_or_organization == authenticated_user_login {
let json_value = github_client.send_to_github_api(github_api_user_repository_new(&github_owner_or_organization, &package_name, &description));
if let Some(error_message) = json_value.get("message") {
eprintln!("{RED}{error_message}{RESET}");
if let Some(errors) = json_value.get("errors") {
let errors = errors.as_array().unwrap();
for error in errors.iter() {
if let Some(code) = error.get("message") {
eprintln!("{RED}{code}{RESET}");
}
}
}
panic!("{RED}Call to GitHub API github_api_user_repository_new returned an error.{RESET}")
}
json_value
} else {
let json_value = github_client.send_to_github_api(github_api_organization_repository_new(&github_owner_or_organization, &package_name, &description));
if let Some(error_message) = json_value.get("message") {
eprintln!("{RED}{error_message}{RESET}");
if let Some(errors) = json_value.get("errors") {
let errors = errors.as_array().unwrap();
for error in errors.iter() {
if let Some(code) = error.get("message") {
eprintln!("{RED}{code}{RESET}");
}
}
}
panic!("{RED}Call to GitHub API github_api_organization_repository_new returned an error.{RESET}")
}
json_value
};
println!("{YELLOW}name: {}{RESET}", json_value.get("name").unwrap().as_str().unwrap());
println!("{YELLOW}description: {}{RESET}", json_value.get("description").unwrap().as_str().unwrap());
let repo_html_url = json_value.get("html_url").unwrap().as_str().unwrap().to_string();
println!("{YELLOW}url: {}{RESET}", &repo_html_url);
cl::ShellCommandLimitedDoubleQuotesSanitizer::new(r#"git remote add origin "git@github.com:{github_owner_or_organization}/{name}.git" "#)
.unwrap()
.arg("{github_owner_or_organization}", &github_owner_or_organization)
.unwrap()
.arg("{name}", &package_name)
.unwrap()
.run()
.unwrap();
}
if !git_has_upstream() {
cl::run_shell_command("git push -u origin main").unwrap_or_else(|e| panic!("{e}"));
let _json = github_client.send_to_github_api(github_api_create_a_github_pages_site(&github_owner_or_organization, &package_name));
}
Some(())
}
pub fn description_and_topics_to_github(github_client: &impl SendToGitHubApi) {
let cargo_toml = cl::CargoToml::read();
let repo_name = cargo_toml.package_name();
let github_owner_or_organization = cargo_toml.github_owner().unwrap();
let description = cargo_toml.package_description().unwrap();
let keywords = cargo_toml.package_keywords();
#[derive(serde::Serialize, serde::Deserialize)]
struct OldMetadata {
old_description: String,
old_keywords: Vec<String>,
}
let mut is_old_metadata_different = true;
if let Ok(old_metadata) = std::fs::read_to_string("automation_tasks_rs/.old_metadata.json") {
if let Ok(old_metadata) = serde_json::from_str::<OldMetadata>(&old_metadata) {
if old_metadata.old_description == description && old_metadata.old_keywords == keywords {
is_old_metadata_different = false;
}
}
}
if is_old_metadata_different {
let json = github_client.send_to_github_api(github_api_get_repository(&github_owner_or_organization, &repo_name));
let gh_description = json.get("description").unwrap().as_str().unwrap();
let gh_topics = json.get("topics").unwrap().as_array().unwrap();
let gh_topics: Vec<String> = gh_topics.into_iter().map(|value| value.as_str().unwrap().to_string()).collect();
if gh_description != description {
let _json = github_client.send_to_github_api(github_api_update_description(&github_owner_or_organization, &repo_name, &description));
}
let topics_is_equal = if gh_topics.len() == keywords.len() {
let mut elements_is_equal = true;
'outer: for x in gh_topics.iter() {
let mut has_element = false;
'inner: for y in keywords.iter() {
if y == x {
has_element = true;
break 'inner;
}
}
if !has_element {
elements_is_equal = false;
break 'outer;
}
}
elements_is_equal
} else {
false
};
if !topics_is_equal {
let _json = github_client.send_to_github_api(github_api_replace_all_topics(&github_owner_or_organization, &repo_name, &keywords));
let old_metadata = OldMetadata {
old_description: description,
old_keywords: keywords,
};
std::fs::write("automation_tasks_rs/.old_metadata.json", serde_json::to_string_pretty(&old_metadata).unwrap()).unwrap();
}
}
}
pub fn github_api_get_authenticated_user() -> reqwest::blocking::RequestBuilder {
let repos_url = format!("https://api.github.com/user");
reqwest::blocking::Client::new()
.get(repos_url.as_str())
.header("Accept", "application/vnd.github+json")
.header("X-GitHub-Api-Version", "2022-11-28")
.header("User-Agent", "cargo_auto_lib")
}
pub fn github_api_get_organization(organization: &str) -> reqwest::blocking::RequestBuilder {
let repos_url = format!("https://api.github.com/orgs/{organization}");
reqwest::blocking::Client::new()
.get(repos_url.as_str())
.header("Accept", "application/vnd.github+json")
.header("X-GitHub-Api-Version", "2022-11-28")
.header("User-Agent", "cargo_auto_lib")
}
pub fn github_api_get_repository(github_owner_or_organization: &str, repo_name: &str) -> reqwest::blocking::RequestBuilder {
let repos_url = format!("https://api.github.com/repos/{github_owner_or_organization}/{repo_name}");
reqwest::blocking::Client::new()
.get(repos_url.as_str())
.header("Accept", "application/vnd.github+json")
.header("X-GitHub-Api-Version", "2022-11-28")
.header("User-Agent", "cargo_auto_lib")
}
pub fn github_api_user_repository_new(github_owner: &str, name: &str, description: &str) -> reqwest::blocking::RequestBuilder {
let repos_url = format!("https://api.github.com/user/repos");
let body = serde_json::json!({
"name": name,
"description": description,
"homepage": format!("https://{github_owner}.github.io/{name}"),
"private":false,
"has_issues":true,
"has_projects":false,
"has_wiki":false,
"has_discussions" :true
});
let body = body.to_string();
reqwest::blocking::Client::new()
.post(repos_url.as_str())
.header("Accept", "application/vnd.github+json")
.header("X-GitHub-Api-Version", "2022-11-28")
.header("User-Agent", "cargo_auto_lib")
.body(body)
}
pub fn github_api_organization_repository_new(organization: &str, name: &str, description: &str) -> reqwest::blocking::RequestBuilder {
let repos_url = format!("https://api.github.com/orgs/{organization}/repos");
let body = serde_json::json!({
"name": name,
"description": description,
"homepage": format!("https://{organization}.github.io/{name}"),
"private":false,
"has_issues":true,
"has_projects":false,
"has_wiki":false,
"has_discussions" :true
});
let body = body.to_string();
reqwest::blocking::Client::new()
.post(repos_url.as_str())
.header("Accept", "application/vnd.github+json")
.header("X-GitHub-Api-Version", "2022-11-28")
.header("User-Agent", "cargo_auto_lib")
.body(body)
}
pub fn github_api_update_description(github_owner_or_organization: &str, repo_name: &str, description: &str) -> reqwest::blocking::RequestBuilder {
let repos_url = format!("https://api.github.com/repos/{github_owner_or_organization}/{repo_name}");
let body = serde_json::json!({
"description": description,
});
let body = body.to_string();
reqwest::blocking::Client::new()
.patch(repos_url.as_str())
.header("Accept", "application/vnd.github+json")
.header("X-GitHub-Api-Version", "2022-11-28")
.header("User-Agent", "cargo_auto_lib")
.body(body)
}
pub fn github_api_replace_all_topics(github_owner_or_organization: &str, repo_name: &str, topics: &Vec<String>) -> reqwest::blocking::RequestBuilder {
let repos_url = format!("https://api.github.com/repos/{github_owner_or_organization}/{repo_name}/topics");
let body = serde_json::json!({
"names": topics,
});
let body = body.to_string();
reqwest::blocking::Client::new()
.put(repos_url.as_str())
.header("Accept", "application/vnd.github+json")
.header("X-GitHub-Api-Version", "2022-11-28")
.header("User-Agent", "cargo_auto_lib")
.body(body)
}
pub fn github_api_create_a_github_pages_site(github_owner_or_organization: &str, repo_name: &str) -> reqwest::blocking::RequestBuilder {
let repos_url = format!("https://api.github.com/repos/{github_owner_or_organization}/{repo_name}/pages");
let body = serde_json::json!({
"build_type": "workflow",
"source": {
"branch": "main",
"path": "/docs"
}
});
let body = body.to_string();
reqwest::blocking::Client::new()
.post(repos_url.as_str())
.header("Accept", "application/vnd.github+json")
.header("X-GitHub-Api-Version", "2022-11-28")
.header("User-Agent", "cargo_auto_lib")
.body(body)
}
pub fn github_api_upload_asset_to_release(github_client: &impl SendToGitHubApi, github_owner_or_organization: &str, repo: &str, release_id: &str, path_to_file: &str) {
println!(" {YELLOW}Uploading file to GitHub release: {path_to_file}{RESET}");
let file = camino::Utf8Path::new(&path_to_file);
let file_name = file.file_name().unwrap();
let release_upload_url = format!("https://uploads.github.com/repos/{github_owner_or_organization}/{repo}/releases/{release_id}/assets");
let mut release_upload_url = <url::Url as std::str::FromStr>::from_str(&release_upload_url).unwrap();
release_upload_url.set_query(Some(format!("{}={}", "name", file_name).as_str()));
let file_size = std::fs::metadata(file).unwrap().len();
println!(" {YELLOW}It can take some time to upload. File size: {file_size}. Wait...{RESET}");
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async move {
let file = tokio::fs::File::open(file).await.unwrap();
let stream = tokio_util::codec::FramedRead::new(file, tokio_util::codec::BytesCodec::new());
let body = reqwest::Body::wrap_stream(stream);
let req = reqwest::Client::new()
.post(release_upload_url.as_str())
.header("Content-Type", "application/octet-stream")
.header("Content-Length", file_size.to_string())
.body(body);
github_client.upload_to_github(req).await;
});
}
pub fn github_api_create_new_release(github_owner_or_organization: &str, repo: &str, tag_name_version: &str, name: &str, branch: &str, body_md_text: &str) -> reqwest::blocking::RequestBuilder {
let releases_url = format!("https://api.github.com/repos/{github_owner_or_organization}/{repo}/releases");
let body = serde_json::json!({
"tag_name": tag_name_version,
"target_commitish":branch,
"name":name,
"body":body_md_text,
"draft":false,
"prerelease":false,
"generate_release_notes":false,
});
let body = body.to_string();
reqwest::blocking::Client::new()
.post(releases_url.as_str())
.header("Content-Type", "application/vnd.github+json")
.header("X-GitHub-Api-Version", "2022-11-28")
.header("User-Agent", "cargo_auto_lib")
.body(body)
}