blob: 3df27f4dc558f4acaa355da9069a423c5ec5b9ba [file] [log] [blame]
//! Utilities for working with gix that might be useful for downstream users
use crate::{error::GitError, Error};
/// Writes the `FETCH_HEAD` for the specified fetch outcome to the specified git
/// repository
///
/// This function is narrowly focused on on writing a `FETCH_HEAD` that contains
/// exactly two pieces of information, the id of the commit pointed to by the
/// remote `HEAD`, and, if it exists, the same id with the remote branch whose
/// `HEAD` is the same. This focus gives use two things:
/// 1. `FETCH_HEAD` that can be parsed to the correct remote HEAD by
/// [`gix`](https://github.com/Byron/gitoxide/commit/eb2b513bd939f6b59891d0a4cf5465b1c1e458b3)
/// 1. A `FETCH_HEAD` that closely (or even exactly) matches that created by
/// cargo via git or git2 when fetching only `+HEAD:refs/remotes/origin/HEAD`
///
/// Calling this function for the fetch outcome of a clone will write `FETCH_HEAD`
/// just as if a normal fetch had occurred, but note that AFAICT neither git nor
/// git2 does this, ie. a fresh clone will not have a `FETCH_HEAD` present. I don't
/// _think_ that has negative implications, but if it does...just don't call this
/// function on the result of a clone :)
///
/// Note that the remote provided should be the same remote used for the fetch
/// operation. The reason this is not just grabbed from the repo is because
/// repositories may not have the configured remote, or the remote was modified
/// (eg. replacing refspecs) before the fetch operation
pub fn write_fetch_head(
repo: &gix::Repository,
fetch: &gix::remote::fetch::Outcome,
remote: &gix::Remote<'_>,
) -> Result<gix::ObjectId, Error> {
use gix::{bstr::ByteSlice, protocol::handshake::Ref};
use std::fmt::Write;
// Find the remote head commit
let (head_target_branch, oid) = fetch
.ref_map
.mappings
.iter()
.find_map(|mapping| {
let gix::remote::fetch::Source::Ref(rref) = &mapping.remote else {
return None;
};
let Ref::Symbolic {
full_ref_name,
target,
object,
..
} = rref
else {
return None;
};
(full_ref_name == "HEAD").then_some((target, object))
})
.ok_or_else(|| GitError::UnableToFindRemoteHead)?;
let remote_url = {
let ru = remote
.url(gix::remote::Direction::Fetch)
.expect("can't fetch without a fetch url");
let s = ru.to_bstring();
let v = s.into();
String::from_utf8(v).expect("remote url was not utf-8 :-/")
};
let fetch_head = {
let mut hex_id = [0u8; 40];
let gix::ObjectId::Sha1(sha1) = oid;
let commit_id = crate::utils::encode_hex(sha1, &mut hex_id);
let mut fetch_head = String::new();
let remote_name = remote
.name()
.and_then(|n| {
let gix::remote::Name::Symbol(name) = n else {
return None;
};
Some(name.as_ref())
})
.unwrap_or("origin");
// We write the remote HEAD first, but _only_ if it was explicitly requested
if remote
.refspecs(gix::remote::Direction::Fetch)
.iter()
.any(|rspec| {
let rspec = rspec.to_ref();
if !rspec.remote().map_or(false, |r| r.ends_with(b"HEAD")) {
return false;
}
rspec.local().map_or(false, |l| {
l.to_str()
.ok()
.and_then(|l| {
l.strip_prefix("refs/remotes/")
.and_then(|l| l.strip_suffix("/HEAD"))
})
.map_or(false, |remote| remote == remote_name)
})
})
{
writeln!(&mut fetch_head, "{commit_id}\t\t{remote_url}").unwrap();
}
// Attempt to get the branch name, but if it looks suspect just skip this,
// it _should_ be fine, or at least, we've already written the only thing
// that gix can currently parse
if let Some(branch_name) = head_target_branch
.to_str()
.ok()
.and_then(|s| s.strip_prefix("refs/heads/"))
{
writeln!(
&mut fetch_head,
"{commit_id}\t\tbranch '{branch_name}' of {remote_url}"
)
.unwrap();
}
fetch_head
};
// We _could_ also emit other branches/tags like git does, however it's more
// complicated than just our limited use case of writing remote HEAD
//
// 1. Remote branches are always emitted, however in gix those aren't part
// of the ref mappings if they haven't been updated since the last fetch
// 2. Conversely, tags are _not_ written by git unless they have been changed
// added, but gix _does_ always place those in the fetch mappings
if fetch_head.is_empty() {
return Err(GitError::UnableToFindRemoteHead.into());
}
let fetch_head_path = crate::PathBuf::from_path_buf(repo.path().join("FETCH_HEAD"))?;
std::fs::write(&fetch_head_path, fetch_head)
.map_err(|io| Error::IoPath(io, fetch_head_path))?;
Ok(*oid)
}