1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
//! 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)
}