Rewrite rust (#38)
Co-authored-by: kjuulh <contact@kjuulh.io> Reviewed-on: #38
This commit is contained in:
36
crates/octopush_core/src/builder/builder_capabilities.rs
Normal file
36
crates/octopush_core/src/builder/builder_capabilities.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
59
crates/octopush_core/src/builder/builders/golang_bin.rs
Normal file
59
crates/octopush_core/src/builder/builders/golang_bin.rs
Normal 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(())
|
||||
}
|
||||
}
|
1
crates/octopush_core/src/builder/builders/mod.rs
Normal file
1
crates/octopush_core/src/builder/builders/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod golang_bin;
|
22
crates/octopush_core/src/builder/mod.rs
Normal file
22
crates/octopush_core/src/builder/mod.rs
Normal 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>;
|
48
crates/octopush_core/src/executor/default_executor.rs
Normal file
48
crates/octopush_core/src/executor/default_executor.rs
Normal 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(())
|
||||
}
|
||||
}
|
17
crates/octopush_core/src/executor/executor.rs
Normal file
17
crates/octopush_core/src/executor/executor.rs
Normal 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>;
|
22
crates/octopush_core/src/executor/executors/golang.rs
Normal file
22
crates/octopush_core/src/executor/executors/golang.rs
Normal 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(())
|
||||
}
|
||||
}
|
1
crates/octopush_core/src/executor/executors/mod.rs
Normal file
1
crates/octopush_core/src/executor/executors/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod golang;
|
3
crates/octopush_core/src/executor/mod.rs
Normal file
3
crates/octopush_core/src/executor/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod default_executor;
|
||||
pub mod executor;
|
||||
mod executors;
|
315
crates/octopush_core/src/git/git.rs
Normal file
315
crates/octopush_core/src/git/git.rs
Normal 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(())
|
||||
}
|
||||
}
|
76
crates/octopush_core/src/git/gitea/client.rs
Normal file
76
crates/octopush_core/src/git/gitea/client.rs
Normal 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(())
|
||||
}
|
||||
}
|
42
crates/octopush_core/src/git/gitea/mod.rs
Normal file
42
crates/octopush_core/src/git/gitea/mod.rs
Normal 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>;
|
79
crates/octopush_core/src/git/gitea/provider.rs
Normal file
79
crates/octopush_core/src/git/gitea/provider.rs
Normal 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
|
||||
}
|
||||
}
|
25
crates/octopush_core/src/git/mod.rs
Normal file
25
crates/octopush_core/src/git/mod.rs
Normal 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>;
|
7
crates/octopush_core/src/lib.rs
Normal file
7
crates/octopush_core/src/lib.rs
Normal 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;
|
2
crates/octopush_core/src/schema/mod.rs
Normal file
2
crates/octopush_core/src/schema/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod models;
|
||||
pub mod parser;
|
73
crates/octopush_core/src/schema/models.rs
Normal file
73
crates/octopush_core/src/schema/models.rs
Normal 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,
|
||||
},
|
||||
}
|
36
crates/octopush_core/src/schema/parser.rs
Normal file
36
crates/octopush_core/src/schema/parser.rs
Normal 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)
|
||||
}
|
||||
}
|
48
crates/octopush_core/src/selectors/git_selector.rs
Normal file
48
crates/octopush_core/src/selectors/git_selector.rs
Normal 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(())
|
||||
}
|
||||
}
|
59
crates/octopush_core/src/selectors/gitea_selector.rs
Normal file
59
crates/octopush_core/src/selectors/gitea_selector.rs
Normal 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(())
|
||||
}
|
||||
}
|
3
crates/octopush_core/src/selectors/mod.rs
Normal file
3
crates/octopush_core/src/selectors/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod git_selector;
|
||||
pub mod gitea_selector;
|
||||
|
50
crates/octopush_core/src/shell/mod.rs
Normal file
50
crates/octopush_core/src/shell/mod.rs
Normal 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(())
|
||||
}
|
54
crates/octopush_core/src/storage/local.rs
Normal file
54
crates/octopush_core/src/storage/local.rs
Normal 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);
|
||||
}
|
||||
}
|
32
crates/octopush_core/src/storage/mod.rs
Normal file
32
crates/octopush_core/src/storage/mod.rs
Normal 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(())
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user