create pull

This commit is contained in:
Kasper Juul Hermansen 2022-11-27 11:55:57 +01:00
parent 09ae29fe9b
commit a2437d1297
Signed by: kjuulh
GPG Key ID: 57B6E1465221F912
2 changed files with 150 additions and 16 deletions

View File

@ -1,7 +1,7 @@
use std::{path::PathBuf, sync::Arc}; use std::{path::PathBuf, sync::Arc};
use eyre::ContextCompat; use eyre::ContextCompat;
use git2::{Cred, PushOptions, RemoteCallbacks, Repository}; use git2::{AutotagOption, Cred, FetchOptions, PushOptions, RemoteCallbacks, Repository};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::storage::DynStorageEngine; use crate::storage::DynStorageEngine;
@ -25,6 +25,112 @@ impl LocalGitProvider {
options, 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] #[async_trait::async_trait]
@ -84,17 +190,14 @@ impl GitProvider for LocalGitProvider {
branch_name: &String, branch_name: &String,
) -> eyre::Result<()> { ) -> eyre::Result<()> {
let repo = repo.lock().await; let repo = repo.lock().await;
let branch_name = branch_name.to_lowercase().replace(" ", "-");
let head_commit_oid = repo let head_commit_oid = repo
.head()? .head()?
.target() .target()
.ok_or(eyre::anyhow!("could not get access to target commit"))?; .ok_or(eyre::anyhow!("could not get access to target commit"))?;
let head_commit = repo.find_commit(head_commit_oid)?; let head_commit = repo.find_commit(head_commit_oid)?;
let newbranch = repo.branch( let newbranch = repo.branch(&branch_name, &head_commit, true)?;
&branch_name.to_lowercase().replace(" ", "-"),
&head_commit,
true,
)?;
repo.set_head( repo.set_head(
newbranch newbranch
@ -103,6 +206,43 @@ impl GitProvider for LocalGitProvider {
.ok_or(eyre::anyhow!("could not get name of reference"))?, .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(()) Ok(())
} }
@ -117,15 +257,6 @@ impl GitProvider for LocalGitProvider {
tracing::trace!("pulling signature from local git"); tracing::trace!("pulling signature from local git");
let signature = repo.signature()?; let signature = repo.signature()?;
for rev in repo.revwalk()? {
let rev = rev?;
let commit = repo.find_commit(rev)?;
let summary = commit
.summary()
.ok_or(eyre::anyhow!("could not find commit"))?;
tracing::info!(summary, "summary")
}
tracing::trace!("fetching index and adding changed files to working tree"); tracing::trace!("fetching index and adding changed files to working tree");
let mut index = repo.index()?; let mut index = repo.index()?;
index.add_all(&["."], git2::IndexAddOption::DEFAULT, None)?; index.add_all(&["."], git2::IndexAddOption::DEFAULT, None)?;

View File

@ -19,13 +19,16 @@ impl StorageEngine for LocalStorageEngine {
async fn allocate_dir(&self) -> eyre::Result<super::TemporaryDir> { async fn allocate_dir(&self) -> eyre::Result<super::TemporaryDir> {
let subdir_name = Standard.sample_string(&mut rand::thread_rng(), 2); let subdir_name = Standard.sample_string(&mut rand::thread_rng(), 2);
let mut path = self.root.clone(); let mut path = self.root.clone();
path.push("tmp");
path.push(hex::encode(subdir_name)); path.push(hex::encode(subdir_name));
Ok(super::TemporaryDir::new(path)) Ok(super::TemporaryDir::new(path))
} }
async fn cleanup(&self) -> eyre::Result<()> { async fn cleanup(&self) -> eyre::Result<()> {
tokio::fs::remove_dir_all(self.root.clone()).await?; let mut path = self.root.clone();
path.push("tmp");
tokio::fs::remove_dir_all(path).await?;
Ok(()) Ok(())
} }