diff --git a/.github/workflows/impactifier.yml b/.github/workflows/impactifier.yml index d7a5877..3742a64 100644 --- a/.github/workflows/impactifier.yml +++ b/.github/workflows/impactifier.yml @@ -51,7 +51,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - ./target/release/impactifier --tracing-level=0 --from-branch=main --to-branch=refactor + ./target/release/impactifier --tracing-level=0 --from-branch=main --to-branch=main # 6. (Optional) Output diff.json for debugging - name: Output diff.json (Debug) diff --git a/Cargo.lock b/Cargo.lock index c39b024..12aa303 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,6 +16,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "anstream" version = "0.6.15" @@ -362,6 +371,7 @@ dependencies = [ "config", "git2", "lazy_static", + "regex", "rhai", "serde", "serde_derive", @@ -697,6 +707,35 @@ dependencies = [ "getrandom", ] +[[package]] +name = "regex" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rhai" version = "1.19.0" diff --git a/Cargo.toml b/Cargo.toml index a2845e1..ecfd2c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,10 +7,11 @@ edition = "2021" [dependencies] anyhow = "1.0.89" -clap = { version = "4.5.16", features = ["derive"] } +clap = { version = "4.5.16", features = ["derive", "env"] } config = "0.14.0" git2 = "0.19.0" lazy_static = "1.5.0" +regex = "1.11.0" rhai = "1.19.0" serde = "1.0.208" serde_derive = "1.0.208" diff --git a/src/cli.rs b/src/cli.rs index 8713831..d70fd27 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Context}; +use anyhow::anyhow; use std::fs::File; use std::io::Write; use std::path::Path; @@ -8,7 +8,6 @@ use git2::Repository; use serde_json::to_string_pretty; use thiserror::Error; use tracing::{error, info, trace, Level}; -use url::Url; use crate::config::Config; use crate::git; @@ -75,14 +74,15 @@ struct Args { #[arg(long, default_value_t=String::from("origin"))] origin: String, -} -// TODO: add more credentials variants -pub enum Credentials<'a> { - UsernamePassword { - username: &'a str, - password: &'a str, - }, + #[arg(long, env = "GIT_SSH_KEY")] + ssh_key_path: Option, + + #[clap(long, env = "GIT_PAT", help = "HTTPS Personal Access Token")] + https_pat: Option, + + #[arg(long, env="GIT_USERNAME", default_value_t=String::from("git"))] + username: String, } pub fn run() -> Result<(), CliError> { @@ -101,16 +101,20 @@ pub fn run() -> Result<(), CliError> { init_registry(cfg.custom_transform_scripts()); trace!("Transform functions initialized successfully"); - // TODO: Retrieve properly from args - let mock_credentials = utils::get_mock_credentials(); - let clone_into = match cfg.options.clone_into.as_deref() { Some(path) => path, None => Path::new("cloned_repository"), }; + let credentials = utils::get_git_credentials(args.ssh_key_path, args.username, args.https_pat); + let repository_retrieval_result = match cfg.repository.url { - Some(url) => try_retrieve_repo_from_url(mock_credentials, &url, clone_into), + Some(url) => { + if let Err(err) = utils::prepare_directory(clone_into) { + return Err(CliError::Unknown { err: Some(err) }); + } + git::clone_repo(&credentials, &url, clone_into).map_err(|err| anyhow!(err)) + } None => match &cfg.repository.path { Some(path) => try_retrieve_repo_from_path(path), None => { @@ -127,7 +131,7 @@ pub fn run() -> Result<(), CliError> { }; trace!("Successfully retrieved repository"); - if let Err(fetch_err) = git::fetch_remote(&repository, &args.origin, &mock_credentials) { + if let Err(fetch_err) = git::fetch_remote(&repository, &args.origin, &credentials) { error!("Failed to fetch remote"); return Err(CliError::Unknown { err: Some(fetch_err), @@ -138,7 +142,7 @@ pub fn run() -> Result<(), CliError> { // TODO: Support other DiffOptions // // Current one is temporary, just for testing purposes - let diff = match git::extract_difference( + let _diff = match git::extract_difference( &repository, &git::DiffOptions::Branches { from: &args.from_branch.unwrap(), @@ -148,18 +152,25 @@ pub fn run() -> Result<(), CliError> { Ok(diff) => diff, Err(err) => { error!("Failed to extract difference"); + // Temporary, for testing purposes + save_run_result(false); return Err(CliError::Unknown { err: Some(err) }); } }; trace!("Successfuly extracted difference"); // Temporary, for testing purposes - let serialized_diff = to_string_pretty(&diff).unwrap(); + save_run_result(true); + + Ok(()) +} + +fn save_run_result(is_success: bool) { + let text = if is_success { "SUCCESS" } else { "FAILURE" }; + let serialized_diff = to_string_pretty(text).unwrap(); let mut file = File::create("./diff.json").unwrap(); file.write_all(serialized_diff.as_bytes()).unwrap(); - - Ok(()) } fn try_retrieve_repo_from_path(path: &Path) -> Result { @@ -178,29 +189,6 @@ fn try_retrieve_repo_from_path(path: &Path) -> Result { } } -fn try_retrieve_repo_from_url( - credentials: &Credentials, - url: &Url, - clone_into: &Path, -) -> Result { - trace!("attempt to start from url-specified repository"); - utils::prepare_directory(&clone_into) - .with_context(|| "Failed to prepare directory for cloning")?; - - trace!("Starting to clone repository"); - let cloned_repo = match git::clone_repo(&credentials, url, &clone_into) { - Ok(repo) => repo, - Err(err) => { - return Err(anyhow!( - "Failed to clone repository from url.\nError: {}", - err - )); - } - }; - - Ok(cloned_repo) -} - fn setup_logging(tracing_level: u8) { let tracing_level = match tracing_level { 0 => Level::TRACE, diff --git a/src/git.rs b/src/git.rs index 8148823..997c3f1 100644 --- a/src/git.rs +++ b/src/git.rs @@ -3,13 +3,11 @@ use serde::Serialize; use std::path::Path; use thiserror::Error; -use git2::{Cred, RemoteCallbacks, Repository}; +use git2::{Cred, CredentialType, RemoteCallbacks, Repository}; use std::str; use tracing::{error, info, trace}; use url::Url; -use crate::cli::Credentials; - #[derive(Error, Debug)] pub enum GitError { #[error("Failed to authorize git request, due to authentication failure. Error:{err}")] @@ -55,11 +53,14 @@ pub fn extract_difference(repo: &Repository, options: &DiffOptions) -> Result Result<()> { +pub fn fetch_remote<'a, F>(repo: &Repository, remote_name: &str, credentials: F) -> Result<()> +where + F: Fn(&str, Option<&str>, CredentialType) -> Result + 'a, +{ let mut remote = repo.find_remote(remote_name)?; let mut callback = RemoteCallbacks::new(); - callback.credentials(|_url, _username_from_url, _allowed_types| credentials.into()); + callback.credentials(credentials); let mut fetch_options = git2::FetchOptions::new(); fetch_options.remote_callbacks(callback); @@ -77,7 +78,7 @@ pub fn extract_difference_branches( to_branch: &str, ) -> Result { // TODO: Those refs values most likely should not be hardcoded - let ref_from = repo.find_reference(&format!("refs/heads/{}", from_branch))?; + let ref_from = repo.find_reference(&format!("refs/remotes/origin/{}", from_branch))?; let ref_to = repo.find_reference(&format!("refs/remotes/origin/{}", to_branch))?; let commit_a = ref_from.peel_to_commit()?; @@ -120,31 +121,19 @@ pub fn open_repo(path: &Path) -> Result { } } -impl Credentials<'_> { - fn into(&self) -> Result { - let credentials = match self { - Credentials::UsernamePassword { username, password } => { - Cred::userpass_plaintext(&username, &password) - } - }; - - match credentials { - Ok(credentials) => Ok(credentials), - Err(err) => Err(err), - } - } -} - -pub fn clone_repo( - credentials: &Credentials, +pub fn clone_repo<'a, F>( + credentials: F, url: &Url, clone_into: &Path, -) -> Result { +) -> Result +where + F: Fn(&str, Option<&str>, CredentialType) -> Result + 'a, +{ info!("start cloning repository"); let mut callbacks = RemoteCallbacks::new(); - callbacks.credentials(|_url, _username_from_url, _allowed_types| credentials.into()); - trace!("Callback credentials set to userpass_plaintext"); + + callbacks.credentials(credentials); let mut builder = git2::build::RepoBuilder::new(); diff --git a/src/transform.rs b/src/transform.rs index 6467ee4..2fe9cf6 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -1,3 +1,4 @@ + use crate::config::CustomStep; use anyhow::Result; use rhai::{Dynamic, Engine, Map, Scope}; @@ -15,6 +16,7 @@ pub struct Context { pub class_name: Option, } +#[allow(dead_code)] pub trait TransformFn { fn execute( &self, diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 36c25f0..7bd8ef1 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,10 +1,10 @@ +use anyhow::Result; +use git2::{Cred, CredentialType}; use std::{fs, path::Path}; use tracing::{info, trace}; -use anyhow::Result; - -use crate::cli::Credentials; pub fn prepare_directory(path: &Path) -> Result<()> { + trace!("Preparing directory for repository cloning"); if path.exists() { if path.read_dir()?.next().is_some() { info!("Directory is not empty, removing existing files..."); @@ -19,9 +19,41 @@ pub fn prepare_directory(path: &Path) -> Result<()> { Ok(()) } -pub fn get_mock_credentials<'a>() -> &'a Credentials<'a> { - &Credentials::UsernamePassword { - username: "wzslr321", - password: "TEST", +pub fn get_git_credentials( + ssh_key_path: Option, + username: String, + https_pat: Option, +) -> impl Fn(&str, Option<&str>, CredentialType) -> Result { + match (&ssh_key_path, &https_pat) { + (None, None) => + trace!("Neither ssh key path, nor https pat was specified. Fallback set to default git credentials"), + (None, Some(_)) => + trace!("HTTPS PAT was specified and will be used for git credentials creation along username: {}", username), + (Some(_), None) => + trace!("SSH Key was specified and will be used for git credentials"), + (Some(_), Some(_)) => + trace!("both SSH Key and HTTPS PAT were specified, but only SSH Key will be used for git credentials"), + }; + + move |_url: &str, _username: Option<&str>, allowed_types: CredentialType| { + if let (None, None) = (&ssh_key_path, &https_pat) { + return git2::Cred::default(); + } + + if let Some(ssh) = &ssh_key_path { + if allowed_types.contains(CredentialType::SSH_KEY) { + Cred::ssh_key(&username, None, Path::new(&ssh), None) + } else { + Err(git2::Error::from_str("Unsupported credential type for SSH")) + } + } else { + if allowed_types.contains(CredentialType::USER_PASS_PLAINTEXT) { + Cred::userpass_plaintext(&username, &https_pat.clone().unwrap()) + } else { + Err(git2::Error::from_str( + "Unsupported credential type for user_pass_plaintext", + )) + } + } } }