Rewrite rust (#38)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing

Co-authored-by: kjuulh <contact@kjuulh.io>
Reviewed-on: #38
This commit is contained in:
2022-11-27 11:21:35 +00:00
parent 0d6e8bc4a0
commit 991861db99
445 changed files with 53358 additions and 2568 deletions

View File

@@ -0,0 +1,23 @@
[package]
name = "octopush_core"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
gitea_client = { path = "../gitea_client" }
async-trait = { workspace = true }
eyre = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
rand = "0.8.5"
hex = "0.4.3"
git2 = { version = "0.15.0", features = [
"vendored-libgit2",
"vendored-openssl",
] }
serde = { version = "1.0.147", features = ["derive"] }
serde_yaml = "0.9.14"

View File

@@ -0,0 +1,36 @@
use std::{path::PathBuf, sync::Arc};
use async_trait::async_trait;
use crate::schema::models::Action;
use super::{
builders::golang_bin::{GolangBinBuild, GolangBinBuildOpts},
Builder, DynRunnableBin,
};
pub struct BuilderCapabilities;
impl BuilderCapabilities {
pub fn new() -> Self {
Self {}
}
}
#[async_trait]
impl Builder for BuilderCapabilities {
async fn build(&self, action_path: &PathBuf, action: &Action) -> eyre::Result<DynRunnableBin> {
match action {
Action::Go { entry } => {
let bin = GolangBinBuild::new()
.build(GolangBinBuildOpts {
entry: entry.clone(),
src_path: action_path.clone(),
})
.await?;
Ok(Arc::new(bin))
}
}
}
}

View File

@@ -0,0 +1,59 @@
use std::path::PathBuf;
use async_trait::async_trait;
use crate::{builder::RunnableBin, shell::execute_shell};
pub struct GolangBinBuildOpts {
pub entry: String,
pub src_path: PathBuf,
}
pub struct GolangBinBuild;
impl GolangBinBuild {
pub fn new() -> Self {
Self {}
}
pub async fn build(&self, opts: GolangBinBuildOpts) -> eyre::Result<GolangBin> {
tracing::trace!(
src = opts.src_path.to_string_lossy().to_string(),
entry = opts.entry,
"build golang_bin"
);
execute_shell(
format!("go build -o dist/bin {}", opts.entry),
Some(opts.src_path.clone()),
)
.await?;
let abs_path = std::fs::canonicalize(opts.src_path.join("dist/bin"))?;
Ok(GolangBin::new(abs_path))
}
}
pub struct GolangBin {
path: PathBuf,
}
impl GolangBin {
fn new(path: PathBuf) -> Self {
Self { path }
}
}
#[async_trait]
impl RunnableBin for GolangBin {
async fn run(&self, victim_path: &PathBuf) -> eyre::Result<()> {
execute_shell(
self.path.to_string_lossy().to_string(),
Some(victim_path.clone()),
)
.await?;
Ok(())
}
}

View File

@@ -0,0 +1 @@
pub mod golang_bin;

View File

@@ -0,0 +1,22 @@
pub mod builder_capabilities;
mod builders;
use std::{path::PathBuf, sync::Arc};
use async_trait::async_trait;
use crate::schema::models::Action;
#[async_trait]
pub trait RunnableBin {
async fn run(&self, victim_path: &PathBuf) -> eyre::Result<()>;
}
pub type DynRunnableBin = Arc<dyn RunnableBin + Send + Sync>;
#[async_trait]
pub trait Builder {
async fn build(&self, action_path: &PathBuf, action: &Action) -> eyre::Result<DynRunnableBin>;
}
pub type DynBuilder = Arc<dyn Builder + Send + Sync>;

View File

@@ -0,0 +1,48 @@
use std::path::PathBuf;
use async_trait::async_trait;
use crate::{builder::DynBuilder, schema::models::Action};
use super::{
executor::Executor,
executors::golang::{GolangExecutor, GolangExecutorOpts},
};
pub struct DefaultExecutor {
builder: DynBuilder,
}
impl DefaultExecutor {
pub fn new(builder: DynBuilder) -> Self {
Self { builder }
}
}
#[async_trait]
impl Executor for DefaultExecutor {
async fn execute(
&self,
victim_path: &PathBuf,
action_path: &PathBuf,
action: &Action,
) -> eyre::Result<()> {
tracing::trace!(
victim_path = victim_path.to_string_lossy().to_string(),
"execute"
);
let bin = self.builder.build(action_path, action).await?;
match action {
Action::Go { .. } => {
GolangExecutor::new()
.execute(GolangExecutorOpts {
bin,
victim_path: victim_path.clone(),
})
.await?
}
}
Ok(())
}
}

View File

@@ -0,0 +1,17 @@
use std::{path::PathBuf, sync::Arc};
use async_trait::async_trait;
use crate::schema::models::Action;
#[async_trait]
pub trait Executor {
async fn execute(
&self,
victim_path: &PathBuf,
action_path: &PathBuf,
action: &Action,
) -> eyre::Result<()>;
}
pub type DynExecutor = Arc<dyn Executor + Send + Sync>;

View File

@@ -0,0 +1,22 @@
use std::path::PathBuf;
use crate::builder::DynRunnableBin;
pub struct GolangExecutorOpts {
pub bin: DynRunnableBin,
pub victim_path: PathBuf,
}
pub struct GolangExecutor;
impl GolangExecutor {
pub fn new() -> Self {
Self {}
}
pub async fn execute(&self, opts: GolangExecutorOpts) -> eyre::Result<()> {
opts.bin.run(&opts.victim_path).await?;
Ok(())
}
}

View File

@@ -0,0 +1 @@
pub mod golang;

View File

@@ -0,0 +1,3 @@
pub mod default_executor;
pub mod executor;
mod executors;

View File

@@ -0,0 +1,315 @@
use std::{path::PathBuf, sync::Arc};
use eyre::ContextCompat;
use git2::{Cred, FetchOptions, PushOptions, RemoteCallbacks, Repository};
use tokio::sync::Mutex;
use crate::storage::DynStorageEngine;
use super::GitProvider;
#[derive(Clone, Debug)]
pub struct LocalGitProviderOptions {
pub http_auth: Option<String>,
}
pub struct LocalGitProvider {
storage_engine: DynStorageEngine,
options: LocalGitProviderOptions,
}
impl LocalGitProvider {
pub fn new(options: LocalGitProviderOptions, storage_engine: DynStorageEngine) -> Self {
Self {
storage_engine,
options,
}
}
fn fast_forward(
repo: &Repository,
lb: &mut git2::Reference,
rc: &git2::AnnotatedCommit,
) -> Result<(), git2::Error> {
let name = match lb.name() {
Some(s) => s.to_string(),
None => String::from_utf8_lossy(lb.name_bytes()).to_string(),
};
let msg = format!("Fast-Forward: Setting {} to id: {}", name, rc.id());
println!("{}", msg);
lb.set_target(rc.id(), &msg)?;
repo.set_head(&name)?;
repo.checkout_head(Some(
git2::build::CheckoutBuilder::default()
// For some reason the force is required to make the working directory actually get updated
// I suspect we should be adding some logic to handle dirty working directory states
// but this is just an example so maybe not.
.force(),
))?;
Ok(())
}
fn normal_merge(
repo: &Repository,
local: &git2::AnnotatedCommit,
remote: &git2::AnnotatedCommit,
) -> Result<(), git2::Error> {
let local_tree = repo.find_commit(local.id())?.tree()?;
let remote_tree = repo.find_commit(remote.id())?.tree()?;
let ancestor = repo
.find_commit(repo.merge_base(local.id(), remote.id())?)?
.tree()?;
let mut idx = repo.merge_trees(&ancestor, &local_tree, &remote_tree, None)?;
if idx.has_conflicts() {
println!("Merge conficts detected...");
repo.checkout_index(Some(&mut idx), None)?;
return Ok(());
}
let result_tree = repo.find_tree(idx.write_tree_to(repo)?)?;
// now create the merge commit
let msg = format!("Merge: {} into {}", remote.id(), local.id());
let sig = repo.signature()?;
let local_commit = repo.find_commit(local.id())?;
let remote_commit = repo.find_commit(remote.id())?;
// Do our merge commit and set current branch head to that commit.
let _merge_commit = repo.commit(
Some("HEAD"),
&sig,
&sig,
&msg,
&result_tree,
&[&local_commit, &remote_commit],
)?;
// Set working tree to match head.
repo.checkout_head(None)?;
Ok(())
}
fn do_merge<'a>(
repo: &'a Repository,
remote_branch: &str,
fetch_commit: git2::AnnotatedCommit<'a>,
) -> Result<(), git2::Error> {
// 1. do a merge analysis
let analysis = repo.merge_analysis(&[&fetch_commit])?;
// 2. Do the appopriate merge
if analysis.0.is_fast_forward() {
println!("Doing a fast forward");
// do a fast forward
let refname = format!("refs/heads/{}", remote_branch);
match repo.find_reference(&refname) {
Ok(mut r) => {
Self::fast_forward(repo, &mut r, &fetch_commit)?;
}
Err(_) => {
// The branch doesn't exist so just set the reference to the
// commit directly. Usually this is because you are pulling
// into an empty repository.
repo.reference(
&refname,
fetch_commit.id(),
true,
&format!("Setting {} to {}", remote_branch, fetch_commit.id()),
)?;
repo.set_head(&refname)?;
repo.checkout_head(Some(
git2::build::CheckoutBuilder::default()
.allow_conflicts(true)
.conflict_style_merge(true)
.force(),
))?;
}
};
} else if analysis.0.is_normal() {
// do a normal merge
let head_commit = repo.reference_to_annotated_commit(&repo.head()?)?;
Self::normal_merge(&repo, &head_commit, &fetch_commit)?;
} else {
println!("Nothing to do...");
}
Ok(())
}
}
#[async_trait::async_trait]
impl GitProvider for LocalGitProvider {
async fn clone_from_url(&self, url: &String) -> eyre::Result<(PathBuf, Repository)> {
let url = url.clone();
tracing::debug!(url, "allocating dir");
let dir = self.storage_engine.allocate_dir().await?;
let options = self.options.clone();
let dirpath = dir.clone().path();
let repo = tokio::task::spawn_blocking(move || {
let mut callbacks = RemoteCallbacks::new();
callbacks.credentials(|url, username_from_url, _allowed_types| {
tracing::debug!(username_from_url, url, "pulling key from ssh-agent");
if let Some(auth) = &options.http_auth {
tracing::trace!(auth, "authenticating");
let (user, pass) = auth
.split_once(":")
.ok_or("http_auth is not formatted correctly")
.unwrap();
Cred::userpass_plaintext(user, pass)
} else {
let username = username_from_url
.context("could not find username_from_url")
.unwrap();
Cred::ssh_key_from_agent(username)
}
});
let mut fo = git2::FetchOptions::new();
fo.remote_callbacks(callbacks);
let checkout_builder = git2::build::CheckoutBuilder::new();
let mut builder = git2::build::RepoBuilder::new();
builder.fetch_options(fo).with_checkout(checkout_builder);
tracing::debug!(
path = dirpath.as_os_str().to_string_lossy().to_string(),
"clone git repo"
);
builder.clone(url.as_str(), dirpath.as_path())
})
.await??;
tracing::debug!("done pulling repo");
Ok((dir.path(), repo))
}
async fn create_branch(
&self,
repo: Arc<Mutex<Repository>>,
branch_name: &String,
) -> eyre::Result<()> {
let repo = repo.lock().await;
let branch_name = branch_name.to_lowercase().replace(" ", "-");
let head_commit_oid = repo
.head()?
.target()
.ok_or(eyre::anyhow!("could not get access to target commit"))?;
let head_commit = repo.find_commit(head_commit_oid)?;
let newbranch = repo.branch(&branch_name, &head_commit, true)?;
repo.set_head(
newbranch
.into_reference()
.name()
.ok_or(eyre::anyhow!("could not get name of reference"))?,
)?;
tracing::trace!("pulling from origin");
let options = self.options.clone();
let remote = "origin";
let mut cb = RemoteCallbacks::new();
cb.credentials(|url, username_from_url, _allowed_types| {
tracing::debug!(username_from_url, url, "pulling key from ssh-agent");
if let Some(auth) = &options.http_auth {
tracing::trace!(auth, "authenticating");
let (user, pass) = auth
.split_once(":")
.ok_or("http_auth is not formatted correctly")
.unwrap();
Cred::userpass_plaintext(user, pass)
} else {
let username = username_from_url.unwrap();
Cred::ssh_key_from_agent(username)
}
});
let mut remote = repo
.find_remote(remote)
.or_else(|_| repo.remote_anonymous(remote))?;
let mut fo = FetchOptions::new();
fo.remote_callbacks(cb);
let head = repo.head()?;
let refspec = &[head
.name()
.ok_or(eyre::anyhow!("could not find head.name"))?];
remote.fetch(refspec, Some(&mut fo), None)?;
let fetch_head = repo.find_reference("FETCH_HEAD")?;
let commit = repo.reference_to_annotated_commit(&fetch_head)?;
Self::do_merge(&repo, &branch_name, commit)?;
Ok(())
}
async fn push_branch(
&self,
repo: Arc<Mutex<Repository>>,
branch_name: &String,
) -> eyre::Result<()> {
let repo = repo.lock().await;
let options = self.options.clone();
tracing::trace!("pulling signature from local git");
let signature = repo.signature()?;
tracing::trace!("fetching index and adding changed files to working tree");
let mut index = repo.index()?;
index.add_all(&["."], git2::IndexAddOption::DEFAULT, None)?;
index.write()?;
let tree = index.write_tree()?;
let tree = repo.find_tree(tree)?;
let parents = repo.head().map(|h| {
h.target()
.ok_or(eyre::anyhow!("could not fetch target"))
.map(|t| repo.find_commit(t))
})???;
tracing::trace!("writing commit object");
repo.commit(
Some("HEAD"),
&signature,
&signature,
branch_name.to_lowercase().replace(" ", "-").as_str(),
&tree,
&[&parents],
)?;
let mut remote = repo.find_remote("origin")?;
let head = repo.head()?;
let refspec = &[head
.name()
.ok_or(eyre::anyhow!("could not find head.name"))?];
let mut remote_callbacks = RemoteCallbacks::new();
remote_callbacks.credentials(|url, username_from_url, _allowed_types| {
tracing::debug!(username_from_url, url, "pulling key from ssh-agent");
if let Some(auth) = &options.http_auth {
tracing::trace!(auth, "authenticating");
let (user, pass) = auth
.split_once(":")
.ok_or("http_auth is not formatted correctly")
.unwrap();
Cred::userpass_plaintext(user, pass)
} else {
let username = username_from_url.unwrap();
Cred::ssh_key_from_agent(username)
}
});
let mut push_options = PushOptions::new();
push_options.remote_callbacks(remote_callbacks);
tracing::trace!("pushing to remote");
remote.push(refspec, Some(&mut push_options))?;
Ok(())
}
}

View File

@@ -0,0 +1,76 @@
use std::sync::Arc;
use async_trait::async_trait;
use gitea_client::{builder::GiteaClientBuilder, models::CreatePullRequestOption};
use super::GiteaClient;
pub struct DefaultGiteaClientOptions {
pub url: String,
pub basicauth: Option<String>,
}
pub struct DefaultGiteaClient {
gitea_client: Arc<gitea_client::client::GiteaClient>,
}
impl DefaultGiteaClient {
pub fn new(options: &DefaultGiteaClientOptions) -> Self {
let mut gitea = GiteaClientBuilder::new().set_base_path(&options.url);
if let Some(basicauth) = options.basicauth.clone() {
if let Some((username, password)) = basicauth.split_once(":") {
gitea = gitea.set_basic_auth(username.into(), Some(password.into()));
}
}
Self {
gitea_client: Arc::new(gitea.build()),
}
}
}
#[async_trait]
impl GiteaClient for DefaultGiteaClient {
async fn get_clone_url(&self, owner: String, repo_name: String) -> eyre::Result<String> {
let repo = self
.gitea_client
.repository()
.get(&owner, &repo_name)
.await?;
let clone_url = repo
.ssh_url
.ok_or(eyre::anyhow!("clone_url is not set for repository"))?;
Ok(clone_url)
}
async fn create_pull_request(
&self,
owner: &String,
repo_name: &String,
pull_request_name: &String,
) -> eyre::Result<()> {
self.gitea_client
.repository()
.create_pull_request(
&owner,
&repo_name,
Some(CreatePullRequestOption {
assignee: None,
assignees: None,
base: Some("main".into()),
body: None,
due_date: None,
head: Some(pull_request_name.to_lowercase().replace(" ", "-")),
labels: None,
milestone: None,
title: Some(pull_request_name.clone()),
}),
)
.await?;
Ok(())
}
}

View File

@@ -0,0 +1,42 @@
pub mod client;
pub mod provider;
use std::{path::PathBuf, sync::Arc};
use async_trait::async_trait;
use git2::Repository;
use tokio::sync::Mutex;
use crate::schema::models::GitPushPullRequest;
#[async_trait]
pub trait GiteaClient {
async fn get_clone_url(&self, owner: String, repo_name: String) -> eyre::Result<String>;
async fn create_pull_request(
&self,
owner: &String,
repo_name: &String,
pull_request_name: &String,
) -> eyre::Result<()>;
}
pub type DynGiteaClient = Arc<dyn GiteaClient + Send + Sync>;
#[async_trait]
pub trait GiteaProvider {
async fn clone_from_qualified(&self, repo: &String) -> eyre::Result<(PathBuf, Repository)>;
async fn create_branch(
&self,
repo: Arc<Mutex<Repository>>,
branch: &GitPushPullRequest,
) -> eyre::Result<()>;
async fn create_pull_request(
&self,
repo: Arc<Mutex<Repository>>,
repo_name: &String,
pull_request: &GitPushPullRequest,
) -> eyre::Result<()>;
}
pub type DynGiteaProvider = Arc<dyn GiteaProvider + Send + Sync>;

View File

@@ -0,0 +1,79 @@
use std::{path::PathBuf, sync::Arc};
use async_trait::async_trait;
use git2::Repository;
use tokio::sync::Mutex;
use crate::{git::DynGitProvider, schema::models::GitPushPullRequest, storage::DynStorageEngine};
use super::{DynGiteaClient, GiteaProvider};
pub struct DefaultGiteaProvider {
git_provider: DynGitProvider,
_storage_engine: DynStorageEngine,
gitea_client: DynGiteaClient,
}
impl DefaultGiteaProvider {
pub fn new(
git_provider: DynGitProvider,
storage_engine: DynStorageEngine,
gitea_client: DynGiteaClient,
) -> Self {
Self {
git_provider,
_storage_engine: storage_engine,
gitea_client,
}
}
}
#[async_trait]
impl GiteaProvider for DefaultGiteaProvider {
async fn clone_from_qualified(&self, repo: &String) -> eyre::Result<(PathBuf, Repository)> {
let (owner, repo_name) = repo
.split_once("/")
.ok_or(eyre::anyhow!("repo is not a valid format"))?;
let clone_url = self
.gitea_client
.get_clone_url(owner.into(), repo_name.into())
.await?;
let (path, repo) = self.git_provider.clone_from_url(&clone_url).await?;
Ok((path, repo))
}
async fn create_branch(
&self,
repo: Arc<Mutex<Repository>>,
pull_request: &GitPushPullRequest,
) -> eyre::Result<()> {
tracing::trace!("creating branch");
self.git_provider
.create_branch(repo, &pull_request.name)
.await
}
async fn create_pull_request(
&self,
repo: Arc<Mutex<Repository>>,
repo_name: &String,
pull_request: &GitPushPullRequest,
) -> eyre::Result<()> {
let (owner, repo_name) = repo_name
.split_once("/")
.ok_or(eyre::anyhow!("repo is not a valid format"))?;
tracing::trace!("push_branch");
self.git_provider
.push_branch(repo, &pull_request.name)
.await?;
tracing::trace!("create_pull_request");
self.gitea_client
.create_pull_request(&owner.into(), &repo_name.into(), &pull_request.name)
.await
}
}

View File

@@ -0,0 +1,25 @@
use std::{path::PathBuf, sync::Arc};
use async_trait::async_trait;
use git2::Repository;
use tokio::sync::Mutex;
pub mod git;
pub mod gitea;
#[async_trait]
pub trait GitProvider {
async fn clone_from_url(&self, url: &String) -> eyre::Result<(PathBuf, Repository)>;
async fn create_branch(
&self,
repo: Arc<Mutex<Repository>>,
branch_name: &String,
) -> eyre::Result<()>;
async fn push_branch(
&self,
repo: Arc<Mutex<Repository>>,
branch_name: &String,
) -> eyre::Result<()>;
}
pub type DynGitProvider = Arc<dyn GitProvider + Send + Sync>;

View File

@@ -0,0 +1,7 @@
pub mod builder;
pub mod executor;
pub mod git;
pub mod schema;
pub mod selectors;
mod shell;
pub mod storage;

View File

@@ -0,0 +1,2 @@
pub mod models;
pub mod parser;

View File

@@ -0,0 +1,73 @@
use serde::{Deserialize, Serialize};
pub type Repository = String;
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub struct GitPushBranch {
pub name: String,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub struct GitPushPullRequest {
pub name: String,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub struct GitPush {
pub branch: GitPushBranch,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub struct Git {
pub push: Option<GitPush>,
pub repositories: Vec<Repository>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub struct GitHubPush {
#[serde(rename = "pull-request")]
pub pull_request: GitPushPullRequest,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub struct GiteaPush {
#[serde(rename = "pull-request")]
pub pull_request: GitPushPullRequest,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub struct GitHub {
pub push: Option<GitHubPush>,
pub repositories: Vec<Repository>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub struct Gitea {
pub push: Option<GiteaPush>,
pub repositories: Vec<Repository>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub struct SelectAction {
pub git: Option<Git>,
pub github: Option<GitHub>,
pub gitea: Option<Gitea>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[serde(tag = "type")]
pub enum Action {
#[serde(rename = "go")]
Go { entry: String },
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "apiVersion")]
pub enum Schema {
#[serde(rename = "action")]
Action {
name: String,
select: SelectAction,
action: Action,
},
}

View File

@@ -0,0 +1,36 @@
use std::{path::PathBuf, sync::Arc};
use async_trait::async_trait;
use super::models::Schema;
#[async_trait]
pub trait SchemaParser {
async fn parse_file(&self, file: PathBuf) -> eyre::Result<Schema>;
}
pub type DynSchemaParser = Arc<dyn SchemaParser + Send + Sync>;
#[derive(Debug)]
pub struct DefaultSchemaParser {}
#[async_trait]
impl SchemaParser for DefaultSchemaParser {
async fn parse_file(&self, file: PathBuf) -> eyre::Result<Schema> {
let file = tokio::fs::read(file).await?;
self.parse(file)
}
}
impl DefaultSchemaParser {
pub fn new() -> Self {
Self {}
}
pub fn parse(&self, contents: Vec<u8>) -> eyre::Result<Schema> {
let schema = serde_yaml::from_slice(contents.as_slice())?;
Ok(schema)
}
}

View File

@@ -0,0 +1,48 @@
use std::{path::PathBuf, sync::Arc};
use tokio::sync::Mutex;
use crate::{
executor::executor::DynExecutor,
git::DynGitProvider,
schema::models::{Action, Git},
};
pub struct GitSelector {
git_provider: DynGitProvider,
executor: DynExecutor,
}
impl GitSelector {
pub fn new(git_provider: DynGitProvider, executor: DynExecutor) -> Self {
Self {
git_provider,
executor,
}
}
pub async fn run(&self, git: &Git, action_path: &PathBuf, action: &Action) -> eyre::Result<()> {
tracing::info!("fetching repos");
for repo in &git.repositories {
let gp = self.git_provider.clone();
let (path, repo) = gp.clone_from_url(repo).await?;
let repo = Arc::new(Mutex::new(repo));
if let Some(push) = &git.push {
self.git_provider
.create_branch(repo.clone(), &push.branch.name)
.await?;
}
self.executor.execute(&path, action_path, action).await?;
if let Some(push) = &git.push {
self.git_provider
.push_branch(repo, &push.branch.name)
.await?;
}
}
Ok(())
}
}

View File

@@ -0,0 +1,59 @@
use std::{path::PathBuf, sync::Arc};
use tokio::sync::Mutex;
use crate::{
executor::executor::DynExecutor,
git::{gitea::DynGiteaProvider, DynGitProvider},
schema::models::{Action, Gitea},
};
pub struct GiteaSelector {
gitea_provider: DynGiteaProvider,
git_provider: DynGitProvider,
executor: DynExecutor,
}
impl GiteaSelector {
pub fn new(
gitea_provider: DynGiteaProvider,
git_provider: DynGitProvider,
executor: DynExecutor,
) -> Self {
Self {
gitea_provider,
git_provider,
executor,
}
}
pub async fn run(
&self,
git: &Gitea,
action_path: &PathBuf,
action: &Action,
) -> eyre::Result<()> {
tracing::info!("fetching repos");
for repo in &git.repositories {
let gp = self.gitea_provider.clone();
let (path, repo) = gp.clone_from_qualified(repo).await?;
let repo = Arc::new(Mutex::new(repo));
if let Some(push) = &git.push {
self.git_provider
.create_branch(repo.clone(), &push.pull_request.name)
.await?;
}
self.executor.execute(&path, action_path, action).await?;
if let Some(push) = &git.push {
self.git_provider
.push_branch(repo, &push.pull_request.name)
.await?;
}
}
Ok(())
}
}

View File

@@ -0,0 +1,3 @@
pub mod git_selector;
pub mod gitea_selector;

View File

@@ -0,0 +1,50 @@
use std::{path::PathBuf, process::Stdio};
use eyre::Context;
use tokio::io::{AsyncBufReadExt, BufReader};
pub async fn execute_shell(cmd: String, path: Option<PathBuf>) -> eyre::Result<()> {
let mut command = tokio::process::Command::new("sh");
let command = command.arg("-c");
let command = if let Some(path) = path {
command.current_dir(path)
} else {
command
};
let command = command.arg(format!("{}", cmd));
let command = command.stdout(Stdio::piped());
let mut child = command.spawn()?;
let stdout = child
.stdout
.take()
.ok_or(eyre::anyhow!("could not take stdout of command"))?;
let mut reader = BufReader::new(stdout).lines();
tokio::spawn(async move {
let status = child
.wait()
.await
.context(eyre::anyhow!("child process encountered an error"))
.unwrap();
if !status.success() {
tracing::error!(
cmd,
status = status.to_string(),
"child program encountered an error"
);
}
});
while let Some(line) = reader.next_line().await? {
tracing::trace!("{}", line)
}
Ok(())
}

View File

@@ -0,0 +1,54 @@
use std::path::PathBuf;
use rand::distributions::{DistString, Standard};
use super::StorageEngine;
pub struct LocalStorageEngine {
root: PathBuf,
}
impl LocalStorageEngine {
pub fn new(root: PathBuf) -> Self {
Self { root }
}
}
#[async_trait::async_trait]
impl StorageEngine for LocalStorageEngine {
async fn allocate_dir(&self) -> eyre::Result<super::TemporaryDir> {
let subdir_name = Standard.sample_string(&mut rand::thread_rng(), 2);
let mut path = self.root.clone();
path.push("tmp");
path.push(hex::encode(subdir_name));
Ok(super::TemporaryDir::new(path))
}
async fn cleanup(&self) -> eyre::Result<()> {
let mut path = self.root.clone();
path.push("tmp");
tokio::fs::remove_dir_all(path).await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use crate::storage::StorageEngine;
use super::LocalStorageEngine;
#[tokio::test]
async fn create_local_storage_engine_and_allocate() {
let local_storage = LocalStorageEngine::new(PathBuf::new());
let dir = local_storage.allocate_dir().await.expect("to allocate dir");
assert_eq!(dir.path().to_string_lossy().len(), 16);
assert_eq!(dir.path().to_string_lossy().is_empty(), false);
}
}

View File

@@ -0,0 +1,32 @@
pub mod local;
use std::{path::PathBuf, sync::Arc};
use async_trait::async_trait;
#[async_trait]
pub trait StorageEngine {
async fn allocate_dir(&self) -> eyre::Result<TemporaryDir>;
async fn cleanup(&self) -> eyre::Result<()>;
}
pub type DynStorageEngine = Arc<dyn StorageEngine + Send + Sync>;
#[derive(Clone, Debug)]
pub struct TemporaryDir {
path: PathBuf,
}
impl TemporaryDir {
pub fn new(path: PathBuf) -> Self {
Self { path }
}
pub fn path(&self) -> PathBuf {
self.path.clone()
}
pub fn cleanup(self) -> eyre::Result<()> {
Ok(())
}
}