use std::borrow::Cow;
use gix_date::SecondsSinceUnixEpoch;
use gix_negotiate::Flags;
use gix_ref::file::ReferenceExt;
use crate::fetch::{refmap, RefMap, Shallow, Tags};
type Queue = gix_revwalk::PriorityQueue<SecondsSinceUnixEpoch, gix_hash::ObjectId>;
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error("We were unable to figure out what objects the server should send after {rounds} round(s)")]
NegotiationFailed { rounds: usize },
#[error(transparent)]
LookupCommitInGraph(#[from] gix_revwalk::graph::get_or_insert_default::Error),
#[error(transparent)]
OpenPackedRefsBuffer(#[from] gix_ref::packed::buffer::open::Error),
#[error(transparent)]
IO(#[from] std::io::Error),
#[error(transparent)]
InitRefIter(#[from] gix_ref::file::iter::loose_then_packed::Error),
#[error(transparent)]
PeelToId(#[from] gix_ref::peel::to_id::Error),
#[error(transparent)]
AlternateRefsAndObjects(Box<dyn std::error::Error + Send + Sync + 'static>),
}
#[must_use]
#[derive(Debug, Clone)]
pub enum Action {
NoChange,
SkipToRefUpdate,
MustNegotiate {
remote_ref_target_known: Vec<bool>,
},
}
#[derive(Debug, Clone, Copy)]
pub struct Round {
pub haves_sent: usize,
pub in_vain: usize,
pub haves_to_send: usize,
pub previous_response_had_at_least_one_in_common: bool,
}
#[allow(clippy::too_many_arguments)]
pub fn mark_complete_and_common_ref<Out, F, E>(
objects: &(impl gix_object::Find + gix_object::FindHeader + gix_object::Exists),
refs: &gix_ref::file::Store,
alternates: impl FnOnce() -> Result<Out, E>,
negotiator: &mut dyn gix_negotiate::Negotiator,
graph: &mut gix_negotiate::Graph<'_, '_>,
ref_map: &RefMap,
shallow: &Shallow,
mapping_is_ignored: impl Fn(&refmap::Mapping) -> bool,
) -> Result<Action, Error>
where
E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
Out: Iterator<Item = (gix_ref::file::Store, F)>,
F: gix_object::Find,
{
let _span = gix_trace::detail!("mark_complete_and_common_ref", mappings = ref_map.mappings.len());
if ref_map.mappings.is_empty() {
return Ok(Action::NoChange);
}
if let Shallow::Deepen(0) = shallow {
return Ok(Action::NoChange);
}
if let Some(refmap::Mapping {
remote: refmap::Source::Ref(crate::handshake::Ref::Unborn { .. }),
..
}) = ref_map.mappings.last().filter(|_| ref_map.mappings.len() == 1)
{
return Ok(Action::SkipToRefUpdate);
}
let mut cutoff_date = None::<SecondsSinceUnixEpoch>;
let mut num_mappings_with_change = 0;
let mut remote_ref_target_known: Vec<bool> = std::iter::repeat_n(false, ref_map.mappings.len()).collect();
let mut remote_ref_included: Vec<bool> = std::iter::repeat_n(false, ref_map.mappings.len()).collect();
for (mapping_idx, mapping) in ref_map.mappings.iter().enumerate() {
let want_id = mapping.remote.as_id();
let have_id = mapping.local.as_ref().and_then(|name| {
let r = refs.find(name).ok()?;
r.target.try_id().map(ToOwned::to_owned)
});
if !mapping_is_ignored(mapping) {
remote_ref_included[mapping_idx] = true;
if want_id.zip(have_id).is_none_or(|(want, have)| want != have) {
num_mappings_with_change += 1;
}
}
if let Some(commit) = want_id
.and_then(|id| graph.get_or_insert_commit(id.into(), |_| {}).transpose())
.transpose()?
{
remote_ref_target_known[mapping_idx] = true;
cutoff_date = cutoff_date.unwrap_or_default().max(commit.commit_time).into();
} else if want_id.is_some_and(|maybe_annotated_tag| objects.exists(maybe_annotated_tag)) {
remote_ref_target_known[mapping_idx] = true;
}
}
if matches!(shallow, Shallow::NoChange) {
if num_mappings_with_change == 0 {
return Ok(Action::NoChange);
} else if remote_ref_target_known
.iter()
.zip(remote_ref_included)
.filter_map(|(known, included)| included.then_some(known))
.all(|known| *known)
{
return Ok(Action::SkipToRefUpdate);
}
}
let mut queue = Queue::new();
mark_all_refs_in_repo(refs, objects, graph, &mut queue, Flags::COMPLETE)?;
for (alt_refs, alt_objs) in alternates().map_err(|err| Error::AlternateRefsAndObjects(err.into()))? {
mark_all_refs_in_repo(&alt_refs, &alt_objs, graph, &mut queue, Flags::COMPLETE)?;
}
let tips = if let Some(cutoff) = cutoff_date {
let tips = Cow::Owned(queue.clone());
mark_recent_complete_commits(&mut queue, graph, cutoff)?;
tips
} else {
Cow::Borrowed(&queue)
};
gix_trace::detail!("mark known_common").into_scope(|| -> Result<_, Error> {
for mapping in ref_map
.mappings
.iter()
.zip(remote_ref_target_known.iter().copied())
.filter_map(|(mapping, known)| (!known).then_some(mapping))
{
let want_id = mapping.remote.as_id();
if let Some(common_id) = want_id
.and_then(|id| graph.get(id).map(|c| (c, id)))
.filter(|(c, _)| c.data.flags.contains(Flags::COMPLETE))
.map(|(_, id)| id)
{
negotiator.known_common(common_id.into(), graph)?;
}
}
Ok(())
})?;
gix_trace::detail!("mark tips", num_tips = tips.len()).into_scope(|| -> Result<_, Error> {
for tip in tips.iter_unordered() {
negotiator.add_tip(*tip, graph)?;
}
Ok(())
})?;
Ok(Action::MustNegotiate {
remote_ref_target_known,
})
}
pub fn make_refmapping_ignore_predicate(fetch_tags: Tags, ref_map: &RefMap) -> impl Fn(&refmap::Mapping) -> bool + '_ {
let tag_refspec_to_ignore = matches!(fetch_tags, Tags::Included)
.then(|| fetch_tags.to_refspec())
.flatten();
move |mapping| {
tag_refspec_to_ignore.is_some_and(|tag_spec| {
mapping
.spec_index
.implicit_index()
.and_then(|idx| ref_map.extra_refspecs.get(idx))
.is_some_and(|spec| spec.to_ref() == tag_spec)
})
}
}
pub fn add_wants(
objects: &impl gix_object::FindHeader,
arguments: &mut crate::fetch::Arguments,
ref_map: &RefMap,
remote_ref_target_known: &[bool],
shallow: &Shallow,
mapping_is_ignored: impl Fn(&refmap::Mapping) -> bool,
) -> bool {
let is_shallow = !matches!(shallow, Shallow::NoChange);
let mut has_want = false;
let wants = ref_map
.mappings
.iter()
.zip(remote_ref_target_known)
.filter_map(|(m, known)| (is_shallow || !*known).then_some(m))
.filter(|m| !mapping_is_ignored(m));
for want in wants {
let id_on_remote = want.remote.as_id();
if !arguments.can_use_ref_in_want() || matches!(want.remote, refmap::Source::ObjectId(_)) {
if let Some(id) = id_on_remote {
arguments.want(id);
has_want = true;
}
} else {
arguments.want_ref(
want.remote
.as_name()
.expect("name available if this isn't an object id"),
);
has_want = true;
}
let id_is_annotated_tag_we_have = id_on_remote
.and_then(|id| objects.try_header(id).ok().flatten().map(|h| (id, h)))
.filter(|(_, h)| h.kind == gix_object::Kind::Tag)
.map(|(id, _)| id);
if let Some(tag_on_remote) = id_is_annotated_tag_we_have {
arguments.have(tag_on_remote);
}
}
has_want
}
fn mark_recent_complete_commits(
queue: &mut Queue,
graph: &mut gix_negotiate::Graph<'_, '_>,
cutoff: SecondsSinceUnixEpoch,
) -> Result<(), Error> {
let _span = gix_trace::detail!("mark_recent_complete", queue_len = queue.len());
while let Some(id) = queue
.peek()
.and_then(|(commit_time, id)| (commit_time >= &cutoff).then_some(*id))
{
queue.pop_value();
let commit = graph.get(&id).expect("definitely set when adding tips or parents");
for parent_id in commit.parents.clone() {
let mut was_complete = false;
if let Some(parent) = graph
.get_or_insert_commit(parent_id, |md| {
was_complete = md.flags.contains(Flags::COMPLETE);
md.flags |= Flags::COMPLETE;
})?
.filter(|_| !was_complete)
{
queue.insert(parent.commit_time, parent_id);
}
}
}
Ok(())
}
fn mark_all_refs_in_repo(
store: &gix_ref::file::Store,
objects: &impl gix_object::Find,
graph: &mut gix_negotiate::Graph<'_, '_>,
queue: &mut Queue,
mark: Flags,
) -> Result<(), Error> {
let _span = gix_trace::detail!("mark_all_refs");
for local_ref in store.iter()?.all()? {
let mut local_ref = local_ref?;
let id = local_ref.peel_to_id_packed(store, objects, store.cached_packed_buffer()?.as_ref().map(|b| &***b))?;
let mut is_complete = false;
if let Some(commit) = graph
.get_or_insert_commit(id, |md| {
is_complete = md.flags.contains(Flags::COMPLETE);
md.flags |= mark;
})?
.filter(|_| !is_complete)
{
queue.insert(commit.commit_time, id);
};
}
Ok(())
}
pub mod one_round {
#[derive(Clone, Debug)]
pub struct State {
pub haves_to_send: usize,
pub(super) seen_ack: bool,
pub(super) in_vain: usize,
pub(super) common_commits: Option<Vec<gix_hash::ObjectId>>,
}
impl State {
pub fn new(connection_is_stateless: bool) -> Self {
State {
haves_to_send: gix_negotiate::window_size(connection_is_stateless, None),
seen_ack: false,
in_vain: 0,
common_commits: connection_is_stateless.then(Vec::new),
}
}
}
impl State {
fn connection_is_stateless(&self) -> bool {
self.common_commits.is_some()
}
pub(super) fn adjust_window_size(&mut self) {
self.haves_to_send = gix_negotiate::window_size(self.connection_is_stateless(), Some(self.haves_to_send));
}
}
}
pub fn one_round(
negotiator: &mut dyn gix_negotiate::Negotiator,
graph: &mut gix_negotiate::Graph<'_, '_>,
state: &mut one_round::State,
arguments: &mut crate::fetch::Arguments,
previous_response: Option<&crate::fetch::Response>,
) -> Result<(Round, bool), Error> {
let mut seen_ack = false;
if let Some(response) = previous_response {
use crate::fetch::response::Acknowledgement;
for ack in response.acknowledgements() {
match ack {
Acknowledgement::Common(id) => {
seen_ack = true;
negotiator.in_common_with_remote(*id, graph)?;
if let Some(common) = &mut state.common_commits {
common.push(*id);
}
}
Acknowledgement::Ready => {
}
Acknowledgement::Nak => {}
}
}
}
if let Some(common) = &mut state.common_commits {
for have_id in common {
arguments.have(have_id);
}
}
let mut haves_added = 0;
for have_id in (0..state.haves_to_send).map_while(|_| negotiator.next_have(graph)) {
arguments.have(have_id?);
haves_added += 1;
}
if seen_ack {
state.in_vain = 0;
}
state.seen_ack |= seen_ack;
state.in_vain += haves_added;
let round = Round {
haves_sent: haves_added,
in_vain: state.in_vain,
haves_to_send: state.haves_to_send,
previous_response_had_at_least_one_in_common: seen_ack,
};
let is_done = haves_added != state.haves_to_send || (state.seen_ack && state.in_vain >= 256);
state.adjust_window_size();
Ok((round, is_done))
}