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: https://git.front.kjuulh.io/kjuulh/octopush/pulls/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,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>;