use std::fmt;
use std::fs::{File, OpenOptions};
use std::future::Future;
use std::io::{Read, Write};
use std::pin::Pin;
use std::process::Output;
use colored::Colorize;
use dusa_collection_utils::core::types::pathtype::PathType;
use dusa_collection_utils::core::types::stringy::Stringy;
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use dusa_collection_utils::core::errors::{ErrorArrayItem, Errors};
#[cfg(target_os = "linux")]
use dusa_collection_utils::platform::functions::{create_hash, truncate};
use crate::encryption::{simple_decrypt, simple_encrypt};
pub const ARTISANCF: &str = "/opt/artisan/artisan.cf";
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Clone)]
pub enum GitServer {
GitHub,
GitLab,
Custom(String), }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd, Eq, Ord)]
pub struct GitAuth {
pub user: Stringy,
pub repo: Stringy,
pub branch: Stringy,
pub server: GitServer,
pub token: Option<Stringy>, }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd)]
pub struct GitCredentials {
pub auth_items: Vec<GitAuth>,
}
#[derive(Debug)]
pub enum GitAction {
Clone {
repo_name: Stringy,
repo_owner: Stringy,
destination: PathType,
repo_branch: Stringy,
server: GitServer,
},
Pull {
target_branch: Stringy,
destination: PathType,
},
Push {
directory: PathType,
},
Stage {
directory: PathType,
files: Vec<String>,
},
Commit {
directory: PathType,
message: Stringy,
},
CheckRemoteAhead {
directory: PathType,
},
Switch {
branch: Stringy,
destination: PathType,
},
SetSafe {
directory: PathType,
},
SetTrack {
directory: PathType,
},
Branch {
directory: PathType,
},
Fetch {
destination: PathType,
},
RevList {
base: String,
target: String,
destination: PathType,
},
}
#[cfg(target_os = "linux")]
impl GitCredentials {
pub async fn new(file: Option<&PathType>) -> Result<Self, ErrorArrayItem> {
match file {
Some(file) => {
if file.exists() {
let encrypted_credentials = Self::read_file(file)?;
let decrypted_vec: Vec<u8> = simple_decrypt(encrypted_credentials.as_bytes())?;
let decrypted_string: String =
String::from_utf8(decrypted_vec).map_err(ErrorArrayItem::from)?;
let data: GitCredentials =
serde_json::from_str(&decrypted_string.replace('\n', ""))?;
Ok(data)
} else {
Err(ErrorArrayItem::new(
Errors::InvalidFile,
"No such file or directory".to_owned(),
))
}
}
None => {
let encrypted_credentials = Self::read_file(&PathType::Str(ARTISANCF.into()))?;
let decrypted_vec: Vec<u8> = simple_decrypt(encrypted_credentials.as_bytes())?;
let decrypted_string: String =
String::from_utf8(decrypted_vec).map_err(ErrorArrayItem::from)?;
let data: GitCredentials = serde_json::from_str(&decrypted_string)?;
Ok(data)
}
}
}
pub async fn new_vec(file: Option<&PathType>) -> Result<Vec<GitAuth>, ErrorArrayItem> {
let git_credentials = Self::new(file).await?;
Ok(git_credentials.auth_items.clone())
}
pub fn to_vec(self) -> Vec<GitAuth> {
self.auth_items
}
pub async fn save(&self, path: &PathType) -> Result<(), ErrorArrayItem> {
if self.clone().to_vec().len() == 0 {
path.delete()?;
}
let json_data = serde_json::to_string(self).map_err(|e| {
ErrorArrayItem::new(Errors::GeneralError, format!("Serialization error: {}", e))
})?;
let encrypted_data = simple_encrypt(json_data.as_bytes())?;
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true) .open(path)
.map_err(|e| {
ErrorArrayItem::new(
Errors::InvalidFile,
format!(
"Unable to create or open the file: {:?}, error: {}",
path, e
),
)
})?;
file.write_all(encrypted_data.as_bytes()).map_err(|e| {
ErrorArrayItem::new(
Errors::ReadingFile,
format!("Unable to write to the file: {:?}, error: {}", path, e),
)
})?;
file.sync_all().map_err(|e| {
ErrorArrayItem::new(
Errors::ReadingFile,
format!("Unable to sync data to the file: {:?}, error: {}", path, e),
)
})?;
Ok(())
}
pub fn read_file(file_path: &PathType) -> Result<Stringy, ErrorArrayItem> {
let mut file = File::open(file_path)?;
let mut file_contents = String::new();
file.read_to_string(&mut file_contents)?;
Ok(Stringy::from(&file_contents.replace('\n', "")))
}
pub fn add_auth(&mut self, auth: GitAuth) {
self.auth_items.push(auth);
}
pub async fn bootstrap_git_credentials() -> Result<GitCredentials, ErrorArrayItem> {
match GitCredentials::new(None).await {
Ok(creds) => Ok(creds),
Err(_) => {
let default_creds = GitCredentials {
auth_items: Vec::new(),
};
Ok(default_creds)
}
}
}
pub async fn delete_item(&mut self, index: usize) -> Result<Self, ErrorArrayItem> {
let mut vec = self.clone().to_vec();
let vec_len = vec.len();
if index > vec_len {
return Err(ErrorArrayItem::new(
Errors::Git,
"The requested value to remove is out of bounds".to_owned(),
));
}
vec.remove(index);
Ok(GitCredentials { auth_items: vec })
}
}
impl GitAuth {
pub fn assemble_remote_url(&self) -> String {
let base_url = match &self.server {
GitServer::GitHub => "https://github.com",
GitServer::GitLab => "https://gitlab.com",
GitServer::Custom(url) => url.trim_end_matches('/'),
};
if let Some(token) = &self.token {
format!(
"https://{}@{}/{}/{}.git",
token, base_url, self.user, self.repo
)
} else {
format!("{}/{}/{}.git", base_url, self.user, self.repo)
}
}
pub fn assemble_remote_ssh(&self) -> String {
match &self.server {
GitServer::GitHub => format!("git@github.com:{}/{}.git", self.user, self.repo),
GitServer::GitLab => format!("git@gitlab.com:{}/{}.git", self.user, self.repo),
GitServer::Custom(host) => {
let host = host.trim_end_matches('/');
format!("git@{}:{}/{}.git", host, self.user, self.repo)
}
}
}
#[cfg(target_os = "linux")]
pub fn generate_id(&self) -> Stringy {
truncate(
&*create_hash(format!("{}-{}-{}", self.branch, self.repo, self.user)),
8,
)
}
}
impl GitAction {
pub fn execute(
&self,
) -> Pin<Box<dyn Future<Output = Result<Option<Output>, ErrorArrayItem>> + '_>> {
Box::pin(async move {
check_git_installed().await?;
match self {
GitAction::Clone {
repo_name,
repo_owner,
destination,
repo_branch,
server,
} => {
let url = match server {
GitServer::GitHub => {
format!("https://github.com/{}/{}.git", repo_owner, repo_name)
}
GitServer::GitLab => {
format!("https://gitlab.com/{}/{}.git", repo_owner, repo_name)
}
GitServer::Custom(base_url) => {
format!(
"{}/{}/{}.git",
base_url.trim_end_matches('/'),
repo_owner,
repo_name
)
}
};
execute_git_command(&[
"clone",
"-b",
repo_branch,
&url,
&destination.to_string(),
])
.await
.map(Some)
}
GitAction::Pull {
target_branch,
destination,
} => {
if destination.exists() {
execute_git_command(&["-C", &destination.to_string(), "pull"]).await?;
execute_git_command(&[
"-C",
&destination.to_string(),
"switch",
target_branch,
])
.await
.map(Some)
} else {
Err(ErrorArrayItem::new(
Errors::InvalidFile,
"Repository path not found".to_string(),
))
}
}
GitAction::Push { directory } => {
if directory.exists() {
execute_git_command(&["-C", &directory.to_string(), "push"])
.await
.map(Some)
} else {
Err(ErrorArrayItem::new(
Errors::InvalidFile,
"Repository path not found".to_string(),
))
}
}
GitAction::Stage { directory, files } => {
let dir = directory.clone().to_string();
if directory.exists() {
let mut args = vec!["-C", &dir, "add"];
args.extend(files.iter().map(|s| s.as_str()));
execute_git_command(&args).await.map(Some)
} else {
Err(ErrorArrayItem::new(
Errors::InvalidFile,
"Repository path not found".to_string(),
))
}
}
GitAction::Commit { directory, message } => {
if directory.exists() {
execute_git_command(&[
"-C",
&directory.to_string(),
"commit",
"-m",
message,
])
.await
.map(Some)
} else {
Err(ErrorArrayItem::new(
Errors::InvalidFile,
"Repository path not found".to_string(),
))
}
}
GitAction::CheckRemoteAhead { directory } => {
let is_ahead = check_remote_ahead(directory).await?;
if is_ahead {
Ok(Some(
Command::new("echo")
.arg("Remote is ahead")
.output()
.await
.map_err(ErrorArrayItem::from)?,
))
} else {
Ok(None)
}
}
GitAction::Fetch { destination } => {
if destination.exists() {
execute_git_command(&["-C", &destination.to_string(), "fetch", "--all"])
.await
.map(|_| None)
} else {
Err(ErrorArrayItem::new(
Errors::InvalidFile,
"Repository path not found".to_string(),
))
}
}
GitAction::Switch {
branch,
destination,
} => {
if destination.exists() {
execute_git_command(&["-C", &destination.to_string(), "switch", branch])
.await
.map(Some)
} else {
Err(ErrorArrayItem::new(
Errors::InvalidFile,
"Repository path not found".to_string(),
))
}
}
GitAction::SetSafe { directory } => execute_git_command(&[
"config",
"--global",
"--add",
"safe.directory",
&directory.to_string(),
])
.await
.map(Some),
GitAction::SetTrack { directory } => {
if directory.exists() {
execute_git_command(&["-C", &directory.to_string(), "fetch"]).await?;
let branch_output = Self::Branch {
directory: directory.clone(),
}
.execute()
.await?;
if let Some(output) = branch_output {
let output_str = String::from_utf8_lossy(&output.stdout);
let branches: Vec<&str> = output_str
.lines()
.filter(|line| !line.contains("->"))
.map(|line| line.trim())
.collect();
for remote in branches {
let clean_remote = remote.replace("origin/", "");
if !clean_remote.is_empty() {
execute_git_command(&[
"-C",
&directory.to_string(),
"branch",
"--track",
&clean_remote,
remote,
])
.await?;
}
}
Ok(None)
} else {
Err(ErrorArrayItem::new(
Errors::Git,
"Invalid branch data from the current repository".to_string(),
))
}
} else {
Err(ErrorArrayItem::new(
Errors::InvalidFile,
"Repository path not found".to_string(),
))
}
}
GitAction::Branch { directory } => {
if directory.exists() {
execute_git_command(&["-C", &directory.to_string(), "branch", "-r"])
.await
.map(Some)
} else {
Err(ErrorArrayItem::new(
Errors::InvalidFile,
"Repository path not found".to_string(),
))
}
}
GitAction::RevList {
base,
target,
destination,
} => {
if destination.exists() {
let rev_list_output = execute_git_command(&[
"-C",
&destination.to_string(),
"rev-list",
"--count",
&format!("{}..{}", base, target),
])
.await?;
Ok(Some(rev_list_output))
} else {
Err(ErrorArrayItem::new(
Errors::InvalidFile,
"Repository path not found".to_string(),
))
}
}
}
})
}
}
async fn check_git_installed() -> Result<(), ErrorArrayItem> {
let output = Command::new("git")
.arg("--version")
.output()
.await
.map_err(ErrorArrayItem::from)?;
if output.status.success() {
Ok(())
} else {
Err(ErrorArrayItem::new(
Errors::GeneralError,
"Git not installed or not found".to_string(),
))
}
}
async fn execute_git_command(args: &[&str]) -> Result<Output, ErrorArrayItem> {
let output = Command::new("git")
.args(args)
.output()
.await
.map_err(|e| ErrorArrayItem::from(e))?;
if output.status.success() {
Ok(output)
} else {
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
Err(ErrorArrayItem::new(Errors::Git, stderr))
}
}
async fn check_remote_ahead(directory: &PathType) -> Result<bool, ErrorArrayItem> {
execute_git_command(&["-C", &directory.to_string(), "fetch"]).await?;
let local_hash =
execute_git_hash_command(&["-C", &directory.to_string(), "rev-parse", "@"]).await?;
let remote_hash =
execute_git_hash_command(&["-C", &directory.to_string(), "rev-parse", "@{u}"]).await?;
Ok(remote_hash != local_hash)
}
async fn execute_git_hash_command(args: &[&str]) -> Result<String, ErrorArrayItem> {
let output = Command::new("git")
.args(args)
.output()
.await
.map_err(ErrorArrayItem::from)?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
Err(ErrorArrayItem::new(Errors::Git, stderr))
}
}
#[cfg(target_os = "linux")]
pub fn generate_git_project_path(auth: &GitAuth) -> PathType {
PathType::Content(format!("/var/www/ais/{}", generate_git_project_id(auth)))
}
#[cfg(target_os = "linux")]
pub fn generate_git_project_id(auth: &GitAuth) -> Stringy {
let hash_input = format!("{}-{}-{}", auth.branch, auth.repo, auth.user);
let hash = create_hash(hash_input);
let truncated_hash = truncate(&*hash, 8);
truncated_hash.into()
}
impl fmt::Display for GitServer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GitServer::GitHub => write!(f, "{}", "GitHub.com".bold().cyan()),
GitServer::GitLab => write!(f, "{}", "GitLab.com".bold().cyan()),
GitServer::Custom(url) => write!(
f,
"{}: {}",
"Custom Server".bold().cyan(),
url.bold().yellow()
),
}
}
}
impl fmt::Display for GitAuth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"{}:",
"Git Authentication Information".bold().underline().purple()
)?;
writeln!(f, " {}: {}", "User".bold().cyan(), self.user)?;
writeln!(f, " {}: {}", "Repository".bold().cyan(), self.repo)?;
writeln!(f, " {}: {}", "Branch".bold().cyan(), self.branch)?;
writeln!(f, " {}: {}", "Server".bold().cyan(), self.server)?;
if let Some(token) = &self.token {
writeln!(
f,
" {}: {}",
"Token (Deprecated stop using)".bold().cyan(),
token
)?;
} else {
writeln!(f, " {}", "Token: None".italic().dimmed())?;
}
Ok(())
}
}
impl fmt::Display for GitCredentials {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "{}:", "Git Credentials".bold().underline().purple())?;
if self.auth_items.is_empty() {
writeln!(
f,
" {}",
"No Git Authentication Items Available".italic().dimmed()
)?;
} else {
for (i, auth) in self.auth_items.iter().enumerate() {
writeln!(f, " {}:", format!("Auth Item {}", i + 1).bold().yellow())?;
writeln!(f, "{}", auth)?;
}
}
Ok(())
}
}