Skip to content

Commit

Permalink
Merge pull request #11 from wzslr321/feature/authentication
Browse files Browse the repository at this point in the history
[Feat]: Git auth
  • Loading branch information
wzslr321 authored Oct 15, 2024
2 parents 147b044 + ff315c9 commit e72822e
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 76 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/impactifier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
39 changes: 39 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
70 changes: 29 additions & 41 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use anyhow::{anyhow, Context};
use anyhow::anyhow;
use std::fs::File;
use std::io::Write;
use std::path::Path;
Expand All @@ -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;
Expand Down Expand Up @@ -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<String>,

#[clap(long, env = "GIT_PAT", help = "HTTPS Personal Access Token")]
https_pat: Option<String>,

#[arg(long, env="GIT_USERNAME", default_value_t=String::from("git"))]
username: String,
}

pub fn run() -> Result<(), CliError> {
Expand All @@ -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 => {
Expand All @@ -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),
Expand All @@ -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(),
Expand All @@ -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<Repository> {
Expand All @@ -178,29 +189,6 @@ fn try_retrieve_repo_from_path(path: &Path) -> Result<Repository> {
}
}

fn try_retrieve_repo_from_url(
credentials: &Credentials,
url: &Url,
clone_into: &Path,
) -> Result<Repository> {
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,
Expand Down
41 changes: 15 additions & 26 deletions src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}")]
Expand Down Expand Up @@ -55,11 +53,14 @@ pub fn extract_difference(repo: &Repository, options: &DiffOptions) -> Result<Di
}
}

pub fn fetch_remote(repo: &Repository, remote_name: &str, credentials: &Credentials) -> Result<()> {
pub fn fetch_remote<'a, F>(repo: &Repository, remote_name: &str, credentials: F) -> Result<()>
where
F: Fn(&str, Option<&str>, CredentialType) -> Result<Cred, git2::Error> + '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);
Expand All @@ -77,7 +78,7 @@ pub fn extract_difference_branches(
to_branch: &str,
) -> Result<Diff> {
// 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()?;
Expand Down Expand Up @@ -120,31 +121,19 @@ pub fn open_repo(path: &Path) -> Result<Repository, GitError> {
}
}

impl Credentials<'_> {
fn into(&self) -> Result<Cred, git2::Error> {
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<Repository, GitError> {
) -> Result<Repository, GitError>
where
F: Fn(&str, Option<&str>, CredentialType) -> Result<Cred, git2::Error> + '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();

Expand Down
2 changes: 2 additions & 0 deletions src/transform.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

use crate::config::CustomStep;
use anyhow::Result;
use rhai::{Dynamic, Engine, Map, Scope};
Expand All @@ -15,6 +16,7 @@ pub struct Context {
pub class_name: Option<String>,
}

#[allow(dead_code)]
pub trait TransformFn {
fn execute(
&self,
Expand Down
46 changes: 39 additions & 7 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -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...");
Expand All @@ -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<String>,
username: String,
https_pat: Option<String>,
) -> impl Fn(&str, Option<&str>, CredentialType) -> Result<Cred, git2::Error> {
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",
))
}
}
}
}

0 comments on commit e72822e

Please sign in to comment.