use gix::prelude::ObjectIdExt;
use gix::refs::transaction::PreviousValue;
use crate::error::{Error, Result};
use crate::git_utils;
use crate::session::Session;
#[must_use]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PushOutput {
pub success: bool,
pub non_fast_forward: bool,
pub up_to_date: bool,
pub remote_name: String,
pub remote_ref: String,
pub commit_oid: String,
}
pub fn push_once(session: &Session, remote: Option<&str>, now: i64) -> Result<PushOutput> {
let repo = &session.repo;
let ns = session.namespace();
let remote_name = git_utils::resolve_meta_remote(repo, remote)?;
let local_ref = session.local_ref();
let remote_refspec = format!("refs/{ns}/main");
let _ = crate::serialize::run(session, now, false)?;
if repo.find_reference(&local_ref).is_err() {
return Err(Error::Other(
"nothing to push (no local metadata ref)".into(),
));
}
let remote_tracking_ref = format!("refs/{ns}/remotes/main");
let mut local_oid = repo
.find_reference(&local_ref)
.ok()
.and_then(|r| r.into_fully_peeled_id().ok())
.map(gix::Id::detach);
let remote_oid = repo
.find_reference(&remote_tracking_ref)
.ok()
.and_then(|r| r.into_fully_peeled_id().ok())
.map(gix::Id::detach);
if let (Some(local), Some(remote_id)) = (local_oid.as_ref(), remote_oid.as_ref()) {
if local == remote_id {
return Ok(PushOutput {
success: true,
non_fast_forward: false,
up_to_date: true,
remote_name,
remote_ref: remote_refspec,
commit_oid: local.to_string(),
});
}
rebase_local_on_remote(repo, &local_ref, &remote_tracking_ref)?;
local_oid = repo
.find_reference(&local_ref)
.ok()
.and_then(|r| r.into_fully_peeled_id().ok())
.map(gix::Id::detach);
}
let commit_oid_str = local_oid
.as_ref()
.map(ToString::to_string)
.unwrap_or_default();
let push_refspec = format!("{local_ref}:{remote_refspec}");
let result = git_utils::run_git(repo, &["push", &remote_name, &push_refspec]);
match result {
Ok(_) => Ok(PushOutput {
success: true,
non_fast_forward: false,
up_to_date: false,
remote_name,
remote_ref: remote_refspec,
commit_oid: commit_oid_str,
}),
Err(e) => {
let err_msg = e.to_string();
let is_non_ff = err_msg.contains("non-fast-forward")
|| err_msg.contains("rejected")
|| err_msg.contains("fetch first");
if is_non_ff {
Ok(PushOutput {
success: false,
non_fast_forward: true,
up_to_date: false,
remote_name,
remote_ref: remote_refspec,
commit_oid: commit_oid_str,
})
} else {
Err(Error::GitCommand(format!("push failed: {err_msg}")))
}
}
}
}
pub fn resolve_push_conflict(session: &Session, remote: Option<&str>, now: i64) -> Result<()> {
let repo = &session.repo;
let ns = session.namespace();
let remote_name = git_utils::resolve_meta_remote(repo, remote)?;
let local_ref = session.local_ref();
let remote_refspec = format!("refs/{ns}/main");
let remote_tracking_ref = format!("refs/{ns}/remotes/main");
let fetch_refspec = format!("{remote_refspec}:{remote_tracking_ref}");
git_utils::run_git(repo, &["fetch", &remote_name, &fetch_refspec])?;
let short_ref = format!("{ns}/remotes/main");
git_utils::hydrate_tip_blobs(repo, &remote_name, &short_ref)?;
let _ = crate::materialize::run(session, None, now)?;
let _ = crate::serialize::run(session, now, false)?;
rebase_local_on_remote(repo, &local_ref, &remote_tracking_ref)?;
Ok(())
}
fn rebase_local_on_remote(repo: &gix::Repository, local_ref: &str, remote_ref: &str) -> Result<()> {
let local_ref_obj = repo
.find_reference(local_ref)
.map_err(|e| Error::Other(format!("{e}")))?;
let local_oid = local_ref_obj
.into_fully_peeled_id()
.map_err(|e| Error::Other(format!("{e}")))?
.detach();
let local_commit_obj = local_oid
.attach(repo)
.object()
.map_err(|e| Error::Other(format!("{e}")))?
.into_commit();
let local_decoded = local_commit_obj
.decode()
.map_err(|e| Error::Other(format!("{e}")))?;
let remote_ref_obj = repo
.find_reference(remote_ref)
.map_err(|e| Error::Other(format!("{e}")))?;
let remote_oid = remote_ref_obj
.into_fully_peeled_id()
.map_err(|e| Error::Other(format!("{e}")))?
.detach();
let parent_ids: Vec<gix::ObjectId> = local_decoded.parents().collect();
if parent_ids.len() == 1 && parent_ids[0] == remote_oid {
return Ok(());
}
let tree_id = local_decoded.tree();
let message = local_decoded.message.to_owned();
let author_ref = local_decoded
.author()
.map_err(|e| Error::Other(format!("{e}")))?;
let commit = gix::objs::Commit {
message,
tree: tree_id,
author: gix::actor::Signature {
name: author_ref.name.into(),
email: author_ref.email.into(),
time: author_ref
.time()
.map_err(|e| Error::Other(format!("{e}")))?,
},
committer: gix::actor::Signature {
name: author_ref.name.into(),
email: author_ref.email.into(),
time: author_ref
.time()
.map_err(|e| Error::Other(format!("{e}")))?,
},
encoding: None,
parents: vec![remote_oid].into(),
extra_headers: Default::default(),
};
let new_oid = repo
.write_object(&commit)
.map_err(|e| Error::Other(format!("{e}")))?
.detach();
repo.reference(
local_ref,
new_oid,
PreviousValue::Any,
"git-meta: rebase for push",
)
.map_err(|e| Error::Other(format!("{e}")))?;
Ok(())
}