use alloc::collections::{BTreeMap, BTreeSet, VecDeque};
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use crate::error::{Error, Result};
use crate::object::{Commit, ObjectType, Tag};
use crate::odb::ObjectDatabase;
use crate::oid::ObjectId;
use crate::protocol::pktline::{self, Packet};
use crate::protocol::{Capabilities, FetchRequest};
use super::daemon::{MAX_NEGOTIATION_BYTES, MAX_NEGOTIATION_PACKETS};
pub const UPLOAD_PACK_CAPABILITIES: &[&str] = &[
"multi_ack",
"thin-pack",
"side-band",
"side-band-64k",
"ofs-delta",
"shallow",
"deepen-since",
"deepen-not",
"deepen-relative",
"no-progress",
"allow-tip-sha1-in-want",
"allow-reachable-sha1-in-want",
"multi_ack_detailed",
"include-tag",
"filter",
];
pub(crate) trait RefView {
fn resolve(&self, name: &str) -> Option<ObjectId>;
fn tags(&self) -> Vec<ObjectId>;
}
pub(crate) struct NoRefs;
impl RefView for NoRefs {
fn resolve(&self, _name: &str) -> Option<ObjectId> {
None
}
fn tags(&self) -> Vec<ObjectId> {
Vec::new()
}
}
const THIN_PACK_BASE_CAP: usize = 2000;
const GIVE_UP_WALK_CAP: usize = 100_000;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum AckMode {
Single,
Multi,
Detailed,
}
impl AckMode {
pub(crate) fn from_caps(caps: &Capabilities) -> Self {
if caps.has("multi_ack_detailed") {
AckMode::Detailed
} else if caps.has("multi_ack") {
AckMode::Multi
} else {
AckMode::Single
}
}
}
pub(crate) struct Negotiator<'a, D: ObjectDatabase> {
odb: &'a D,
wants: &'a [ObjectId],
mode: AckMode,
common: Vec<ObjectId>,
common_set: BTreeSet<ObjectId>,
got_common: bool,
got_other: bool,
}
impl<'a, D: ObjectDatabase> Negotiator<'a, D> {
pub(crate) fn new(odb: &'a D, wants: &'a [ObjectId], mode: AckMode) -> Self {
Negotiator {
odb,
wants,
mode,
common: Vec::new(),
common_set: BTreeSet::new(),
got_common: false,
got_other: false,
}
}
pub(crate) fn common(&self) -> &[ObjectId] {
&self.common
}
pub(crate) fn have(&mut self, oid: ObjectId, out: &mut Vec<u8>) -> Result<()> {
if !self.odb.contains(&oid) {
self.got_other = true;
return Ok(());
}
self.got_common = true;
if self.common_set.insert(oid) {
self.common.push(oid);
}
let line = match self.mode {
AckMode::Detailed => Some(alloc::format!("ACK {} common\n", oid.to_hex())),
AckMode::Multi => Some(alloc::format!("ACK {} continue\n", oid.to_hex())),
AckMode::Single if self.common.len() == 1 => {
Some(alloc::format!("ACK {}\n", oid.to_hex()))
}
AckMode::Single => None,
};
if let Some(line) = line {
out.extend_from_slice(&pktline::encode_data(line.as_bytes())?);
}
Ok(())
}
pub(crate) fn flush(&mut self, out: &mut Vec<u8>) -> Result<()> {
if self.mode == AckMode::Detailed
&& self.got_common
&& !self.got_other
&& ok_to_give_up(self.odb, self.wants, &self.common_set)?
&& let Some(last) = self.common.last()
{
out.extend_from_slice(&pktline::encode_data(
alloc::format!("ACK {} ready\n", last.to_hex()).as_bytes(),
)?);
}
if self.common.is_empty() || self.mode != AckMode::Single {
out.extend_from_slice(&pktline::encode_data(b"NAK\n")?);
}
self.got_common = false;
self.got_other = false;
Ok(())
}
pub(crate) fn done(&mut self, out: &mut Vec<u8>) -> Result<()> {
match self.common.last() {
Some(last) => {
if self.mode != AckMode::Single {
out.extend_from_slice(&pktline::encode_data(
alloc::format!("ACK {}\n", last.to_hex()).as_bytes(),
)?);
}
}
None => out.extend_from_slice(&pktline::encode_data(b"NAK\n")?),
}
Ok(())
}
}
pub(crate) fn ok_to_give_up<D: ObjectDatabase>(
odb: &D,
wants: &[ObjectId],
common: &BTreeSet<ObjectId>,
) -> Result<bool> {
if common.is_empty() {
return Ok(false);
}
for want in wants {
let Some(start) = peel_to_commit(odb, want)? else {
continue;
};
if !reaches_any(odb, &start, common)? {
return Ok(false);
}
}
Ok(true)
}
fn peel_to_commit<D: ObjectDatabase>(odb: &D, id: &ObjectId) -> Result<Option<ObjectId>> {
let mut cur = *id;
for _ in 0..64 {
let (ty, payload) = match odb.read(&cur) {
Ok(v) => v,
Err(Error::NotFound(_)) => return Ok(None),
Err(e) => return Err(e),
};
match ty {
ObjectType::Commit => return Ok(Some(cur)),
ObjectType::Tag => cur = Tag::parse(odb.algo(), &payload)?.object,
_ => return Ok(None),
}
}
Ok(None)
}
fn reaches_any<D: ObjectDatabase>(
odb: &D,
start: &ObjectId,
targets: &BTreeSet<ObjectId>,
) -> Result<bool> {
let mut seen = BTreeSet::new();
let mut queue = VecDeque::from([*start]);
while let Some(id) = queue.pop_front() {
if targets.contains(&id) {
return Ok(true);
}
if !seen.insert(id) || seen.len() > GIVE_UP_WALK_CAP {
continue;
}
if let Some(commit) = read_commit(odb, &id)? {
queue.extend(commit.parents);
}
}
Ok(false)
}
fn read_commit<D: ObjectDatabase>(odb: &D, id: &ObjectId) -> Result<Option<Commit>> {
match odb.read_typed(id, ObjectType::Commit) {
Ok(payload) => Ok(Some(Commit::parse(odb.algo(), &payload)?)),
Err(Error::NotFound(_)) | Err(Error::UnexpectedType { .. }) => Ok(None),
Err(e) => Err(e),
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct Deepen {
pub depth: Option<u32>,
pub since: Option<i64>,
pub not: Vec<String>,
pub relative: bool,
}
impl Deepen {
pub(crate) fn is_active(&self) -> bool {
self.depth.is_some() || self.since.is_some() || !self.not.is_empty()
}
pub(crate) fn from_request(req: &FetchRequest) -> Self {
Deepen {
depth: req.deepen,
since: req.deepen_since,
not: req.deepen_not.clone(),
relative: req.deepen_relative,
}
}
}
pub(crate) struct Want<'a> {
pub wants: &'a [ObjectId],
pub haves: &'a [ObjectId],
pub client_shallow: &'a [ObjectId],
pub deepen: &'a Deepen,
}
pub(crate) struct Plan {
pub objects: BTreeSet<ObjectId>,
pub client_has: BTreeSet<ObjectId>,
pub shallow: Vec<ObjectId>,
pub unshallow: Vec<ObjectId>,
}
pub(crate) fn plan<D: ObjectDatabase>(odb: &D, refs: &dyn RefView, req: &Want<'_>) -> Result<Plan> {
let client_shallow: BTreeSet<ObjectId> = req.client_shallow.iter().copied().collect();
let haves: Vec<ObjectId> = req
.haves
.iter()
.copied()
.filter(|h| odb.contains(h))
.collect();
let client_has = if client_shallow.is_empty() {
crate::walk::reachable_objects(odb, &haves)?
} else {
let mut view =
crate::walk::shallow_objects_to_send(odb, &haves, u32::MAX, &client_shallow)?.objects;
view.retain(|id| odb.contains(id));
view
};
if !req.deepen.is_active() {
let mut objects = crate::walk::reachable_objects_excluding(odb, req.wants, &client_has)?;
objects.extend(req.wants.iter().filter(|w| !req.haves.contains(w)).copied());
return Ok(Plan {
objects,
client_has,
shallow: Vec::new(),
unshallow: Vec::new(),
});
}
let mut extra: BTreeSet<ObjectId> = BTreeSet::new();
let mut tips: Vec<ObjectId> = Vec::new();
for want in req.wants {
collect_tip(odb, want, &mut tips, &mut extra)?;
}
let (included, boundary) = match (req.deepen.depth, req.deepen.relative) {
(Some(depth), false) => depth_walk(odb, &tips, depth)?,
(Some(depth), true) => {
let starts: Vec<ObjectId> = client_shallow
.iter()
.copied()
.filter(|c| odb.contains(c))
.collect();
let (_, new_boundary) = depth_walk(odb, &starts, depth.saturating_add(1))?;
walk_until(odb, &tips, &new_boundary)?
}
(None, _) => rev_list_walk(odb, refs, &tips, req.deepen)?,
};
let mut roots: Vec<ObjectId> = Vec::new();
for c in &included {
if let Some(commit) = read_commit(odb, c)? {
roots.push(commit.tree);
}
}
let mut objects = crate::walk::reachable_objects_excluding(odb, &roots, &client_has)?;
for c in &included {
if !client_has.contains(c) {
objects.insert(*c);
}
}
for e in &extra {
if !client_has.contains(e) {
objects.extend(crate::walk::reachable_objects_excluding(
odb,
&[*e],
&client_has,
)?);
}
}
objects.extend(req.wants.iter().filter(|w| !req.haves.contains(w)).copied());
let shallow = boundary
.iter()
.filter(|c| !client_shallow.contains(c))
.copied()
.collect();
let unshallow = req
.client_shallow
.iter()
.filter(|c| included.contains(c) && !boundary.contains(c))
.copied()
.collect();
Ok(Plan {
objects,
client_has,
shallow,
unshallow,
})
}
fn collect_tip<D: ObjectDatabase>(
odb: &D,
want: &ObjectId,
tips: &mut Vec<ObjectId>,
extra: &mut BTreeSet<ObjectId>,
) -> Result<()> {
let mut cur = *want;
for _ in 0..64 {
let (ty, payload) = match odb.read(&cur) {
Ok(v) => v,
Err(Error::NotFound(_)) => return Ok(()),
Err(e) => return Err(e),
};
match ty {
ObjectType::Commit => {
tips.push(cur);
return Ok(());
}
ObjectType::Tag => {
extra.insert(cur);
cur = Tag::parse(odb.algo(), &payload)?.object;
}
_ => {
extra.insert(cur);
return Ok(());
}
}
}
Ok(())
}
fn depth_walk<D: ObjectDatabase>(
odb: &D,
tips: &[ObjectId],
depth: u32,
) -> Result<(BTreeSet<ObjectId>, BTreeSet<ObjectId>)> {
let depth = depth.max(1);
let mut best: BTreeMap<ObjectId, u32> = BTreeMap::new();
let mut queue: VecDeque<(ObjectId, u32)> = tips.iter().map(|t| (*t, 1)).collect();
while let Some((id, d)) = queue.pop_front() {
if best.get(&id).is_some_and(|&prev| prev <= d) {
continue;
}
let Some(commit) = read_commit(odb, &id)? else {
continue;
};
best.insert(id, d);
if d < depth {
for p in commit.parents {
queue.push_back((p, d + 1));
}
}
}
let included: BTreeSet<ObjectId> = best.keys().copied().collect();
let boundary = best
.iter()
.filter(|&(_, &d)| d >= depth)
.map(|(id, _)| *id)
.collect();
Ok((included, boundary))
}
fn walk_until<D: ObjectDatabase>(
odb: &D,
tips: &[ObjectId],
stop: &BTreeSet<ObjectId>,
) -> Result<(BTreeSet<ObjectId>, BTreeSet<ObjectId>)> {
let mut included = BTreeSet::new();
let mut boundary = BTreeSet::new();
let mut stack: Vec<ObjectId> = tips.to_vec();
while let Some(id) = stack.pop() {
if included.contains(&id) {
continue;
}
let Some(commit) = read_commit(odb, &id)? else {
continue;
};
included.insert(id);
if stop.contains(&id) {
boundary.insert(id);
continue;
}
stack.extend(commit.parents);
}
Ok((included, boundary))
}
fn rev_list_walk<D: ObjectDatabase>(
odb: &D,
refs: &dyn RefView,
tips: &[ObjectId],
deepen: &Deepen,
) -> Result<(BTreeSet<ObjectId>, BTreeSet<ObjectId>)> {
let mut not_roots = Vec::new();
for name in &deepen.not {
let id = refs
.resolve(name)
.ok_or_else(|| Error::Protocol(alloc::format!("deepen-not: {name} is not a ref")))?;
if let Some(c) = peel_to_commit(odb, &id)? {
not_roots.push(c);
}
}
let excluded = commit_ancestry(odb, ¬_roots)?;
let mut included = BTreeSet::new();
let mut parents_of: BTreeMap<ObjectId, Vec<ObjectId>> = BTreeMap::new();
let mut stack: Vec<ObjectId> = tips.to_vec();
while let Some(id) = stack.pop() {
if included.contains(&id) || excluded.contains(&id) {
continue;
}
let Some(commit) = read_commit(odb, &id)? else {
continue;
};
if deepen
.since
.is_some_and(|since| commit.committer.time < since)
{
continue;
}
included.insert(id);
stack.extend(commit.parents.iter().copied());
parents_of.insert(id, commit.parents);
}
if included.is_empty() {
return Err(Error::Protocol(
"no commits selected for shallow requests".to_string(),
));
}
let boundary = parents_of
.iter()
.filter(|(_, parents)| parents.iter().any(|p| !included.contains(p)))
.map(|(id, _)| *id)
.collect();
Ok((included, boundary))
}
fn commit_ancestry<D: ObjectDatabase>(odb: &D, roots: &[ObjectId]) -> Result<BTreeSet<ObjectId>> {
let mut seen = BTreeSet::new();
let mut stack: Vec<ObjectId> = roots.to_vec();
while let Some(id) = stack.pop() {
if !seen.insert(id) {
continue;
}
if let Some(commit) = read_commit(odb, &id)? {
stack.extend(commit.parents);
}
}
Ok(seen)
}
pub(crate) fn include_tags<D: ObjectDatabase>(
odb: &D,
refs: &dyn RefView,
plan: &mut Plan,
) -> Result<()> {
for tag in refs.tags() {
let mut chain = Vec::new();
let mut cur = tag;
for _ in 0..64 {
match odb.read(&cur) {
Ok((ObjectType::Tag, payload)) => {
chain.push(cur);
cur = Tag::parse(odb.algo(), &payload)?.object;
}
_ => break,
}
}
if !chain.is_empty() && plan.objects.contains(&cur) {
for t in chain {
if !plan.client_has.contains(&t) {
plan.objects.insert(t);
}
}
}
}
Ok(())
}
pub(crate) fn build_pack<D: ObjectDatabase>(
odb: &D,
plan: &Plan,
filter: Option<&str>,
wanted: &[ObjectId],
thin: bool,
) -> Result<Vec<u8>> {
use crate::pack::PackWriter;
let filter = crate::object_filter::from_spec(filter);
let wanted: BTreeSet<ObjectId> = wanted.iter().copied().collect();
let mut writer = PackWriter::new(odb.algo());
for id in &plan.objects {
let (ty, payload) = odb.read(id)?;
if !wanted.contains(id) && !filter.keeps(ty, payload.len()) {
continue;
}
writer.add(ty, &payload);
}
if thin {
for id in plan.client_has.iter().take(THIN_PACK_BASE_CAP) {
if let Ok((ty, payload)) = odb.read(id) {
writer.add_external_base(ty, &payload);
}
}
}
Ok(writer.finish()?.pack)
}
pub(crate) fn encode_pack_stream(
out: &mut Vec<u8>,
caps: &Capabilities,
pack: &[u8],
) -> Result<()> {
let max = if caps.has("side-band-64k") {
pktline::MAX_PKTLINE_LEN - 5
} else if caps.has("side-band") {
1000 - 5
} else {
out.extend_from_slice(pack);
return Ok(());
};
for chunk in pack.chunks(max) {
let mut line = Vec::with_capacity(chunk.len() + 1);
line.push(1);
line.extend_from_slice(chunk);
out.extend_from_slice(&pktline::encode_data(&line)?);
}
out.extend_from_slice(&pktline::encode_control(&Packet::Flush));
Ok(())
}
pub(crate) fn encode_shallow_section(out: &mut Vec<u8>, plan: &Plan) -> Result<()> {
for oid in &plan.shallow {
out.extend_from_slice(&pktline::encode_data(
alloc::format!("shallow {}\n", oid.to_hex()).as_bytes(),
)?);
}
for oid in &plan.unshallow {
out.extend_from_slice(&pktline::encode_data(
alloc::format!("unshallow {}\n", oid.to_hex()).as_bytes(),
)?);
}
out.extend_from_slice(&pktline::encode_control(&Packet::Flush));
Ok(())
}
pub(crate) fn serve_stateless<D: ObjectDatabase>(
odb: &D,
refs: &dyn RefView,
request: &[u8],
) -> Result<Vec<u8>> {
let packets = pktline::decode_all(request)?;
let want_end = packets
.iter()
.position(|p| matches!(p, Packet::Flush))
.map_or(packets.len(), |i| i + 1);
let req = FetchRequest::parse(odb.algo(), &packets[..want_end])?;
if req.wants.is_empty() {
return Err(Error::Protocol("upload-pack: request has no wants".into()));
}
let deepen = Deepen::from_request(&req);
let mut out = Vec::new();
let want = Want {
wants: &req.wants,
haves: &[],
client_shallow: &req.shallows,
deepen: &deepen,
};
if deepen.is_active() {
let p = plan(odb, refs, &want)?;
encode_shallow_section(&mut out, &p)?;
}
let mut neg = Negotiator::new(odb, &req.wants, AckMode::from_caps(&req.capabilities));
let mut done = false;
let mut saw_round = false;
for packet in &packets[want_end..] {
match packet {
Packet::Data(d) => {
let line = trim_newline(d);
if let Some(hex) = line.strip_prefix(b"have ") {
let hex = core::str::from_utf8(hex)
.map_err(|_| Error::Protocol("have: non-utf8".into()))?;
neg.have(ObjectId::from_hex(odb.algo(), hex.trim())?, &mut out)?;
saw_round = true;
} else if line == b"done" {
done = true;
break;
}
}
Packet::Flush => {
neg.flush(&mut out)?;
saw_round = false;
}
_ => {}
}
}
if !done {
if saw_round {
neg.flush(&mut out)?;
}
return Ok(out);
}
neg.done(&mut out)?;
let want = Want {
haves: neg.common(),
..want
};
let mut p = plan(odb, refs, &want)?;
if req.capabilities.has("include-tag") {
include_tags(odb, refs, &mut p)?;
}
let thin = req.capabilities.has("thin-pack");
let pack = build_pack(odb, &p, req.filter.as_deref(), &req.wants, thin)?;
encode_pack_stream(&mut out, &req.capabilities, &pack)?;
Ok(out)
}
pub(crate) fn serve_session<S, D>(stream: &mut S, odb: &D, refs: &dyn RefView) -> Result<()>
where
S: std::io::Read + std::io::Write,
D: ObjectDatabase,
{
let mut want_bytes = Vec::new();
loop {
let (packet, raw) = match super::daemon::read_pktline(stream) {
Ok(v) => v,
Err(_) => return Ok(()), };
if want_bytes.len() + raw.len() > MAX_NEGOTIATION_BYTES {
return Err(Error::Protocol(
"upload-pack: want section too large".into(),
));
}
want_bytes.extend_from_slice(&raw);
if matches!(packet, Packet::Flush) {
break;
}
}
let req = FetchRequest::parse(odb.algo(), &pktline::decode_all(&want_bytes)?)?;
if req.wants.is_empty() {
return Ok(()); }
let deepen = Deepen::from_request(&req);
let mut want = Want {
wants: &req.wants,
haves: &[],
client_shallow: &req.shallows,
deepen: &deepen,
};
if deepen.is_active() {
let mut out = Vec::new();
encode_shallow_section(&mut out, &plan(odb, refs, &want)?)?;
write_all(stream, &out)?;
}
let mut neg = Negotiator::new(odb, &req.wants, AckMode::from_caps(&req.capabilities));
let (mut packets, mut bytes) = (0usize, 0usize);
loop {
let (packet, raw) = match super::daemon::read_pktline(stream) {
Ok(v) => v,
Err(_) => return Ok(()), };
packets += 1;
bytes += raw.len();
if packets > MAX_NEGOTIATION_PACKETS || bytes > MAX_NEGOTIATION_BYTES {
return Err(Error::Protocol("upload-pack: negotiation too long".into()));
}
let mut out = Vec::new();
match packet {
Packet::Data(d) => {
let line = trim_newline(&d);
if let Some(hex) = line.strip_prefix(b"have ") {
let hex = core::str::from_utf8(hex)
.map_err(|_| Error::Protocol("have: non-utf8".into()))?;
neg.have(ObjectId::from_hex(odb.algo(), hex.trim())?, &mut out)?;
} else if line == b"done" {
neg.done(&mut out)?;
write_all(stream, &out)?;
break;
} else {
return Err(Error::Protocol(alloc::format!(
"upload-pack: unexpected line {:?}",
String::from_utf8_lossy(line)
)));
}
}
Packet::Flush => neg.flush(&mut out)?,
_ => {}
}
if !out.is_empty() {
write_all(stream, &out)?;
}
}
let common = neg.common().to_vec();
want.haves = &common;
let mut p = plan(odb, refs, &want)?;
if req.capabilities.has("include-tag") {
include_tags(odb, refs, &mut p)?;
}
let thin = req.capabilities.has("thin-pack");
let pack = build_pack(odb, &p, req.filter.as_deref(), &req.wants, thin)?;
let mut out = Vec::new();
encode_pack_stream(&mut out, &req.capabilities, &pack)?;
write_all(stream, &out)
}
fn write_all<S: std::io::Write>(stream: &mut S, bytes: &[u8]) -> Result<()> {
stream
.write_all(bytes)
.and_then(|_| stream.flush())
.map_err(|e| Error::Io(e.to_string()))
}
fn trim_newline(line: &[u8]) -> &[u8] {
line.strip_suffix(b"\n").unwrap_or(line)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::odb::MemoryOdb;
use crate::oid::HashAlgo;
fn chain(n: usize) -> (MemoryOdb, Vec<ObjectId>) {
let odb = MemoryOdb::new(HashAlgo::Sha1);
let tree = odb.write(ObjectType::Tree, b"").unwrap();
let mut ids: Vec<ObjectId> = Vec::new();
for i in 0..n {
let mut body = alloc::format!("tree {}\n", tree.to_hex());
if let Some(p) = ids.last() {
body.push_str(&alloc::format!("parent {}\n", p.to_hex()));
}
body.push_str(&alloc::format!(
"author A <a@e> {i} +0000\ncommitter A <a@e> {i} +0000\n\nc{i}\n"
));
ids.push(odb.write(ObjectType::Commit, body.as_bytes()).unwrap());
}
(odb, ids)
}
fn lines(out: &[u8]) -> Vec<String> {
pktline::decode_all(out)
.unwrap()
.into_iter()
.filter_map(|p| match p {
Packet::Data(d) => Some(String::from_utf8_lossy(trim_newline(&d)).into_owned()),
_ => None,
})
.collect()
}
#[test]
fn negotiator_matches_git_ack_modes() {
let (odb, ids) = chain(3);
let unknown = ObjectId::from_hex(HashAlgo::Sha1, &"7".repeat(40)).unwrap();
let tip = [ids[2]];
let (a, b) = (ids[0].to_hex(), ids[1].to_hex());
let mut out = Vec::new();
let mut n = Negotiator::new(&odb, &tip, AckMode::Detailed);
n.have(ids[1], &mut out).unwrap();
n.have(ids[0], &mut out).unwrap();
n.flush(&mut out).unwrap();
n.done(&mut out).unwrap();
assert_eq!(
lines(&out),
[
alloc::format!("ACK {b} common"),
alloc::format!("ACK {a} common"),
alloc::format!("ACK {a} ready"),
"NAK".into(),
alloc::format!("ACK {a}"),
]
);
let mut out = Vec::new();
let mut n = Negotiator::new(&odb, &tip, AckMode::Detailed);
n.have(unknown, &mut out).unwrap();
n.have(ids[1], &mut out).unwrap();
n.flush(&mut out).unwrap();
assert_eq!(
lines(&out),
[alloc::format!("ACK {b} common"), "NAK".into()]
);
let mut out = Vec::new();
let mut n = Negotiator::new(&odb, &tip, AckMode::Multi);
n.have(ids[1], &mut out).unwrap();
n.flush(&mut out).unwrap();
n.done(&mut out).unwrap();
assert_eq!(
lines(&out),
[
alloc::format!("ACK {b} continue"),
"NAK".into(),
alloc::format!("ACK {b}")
]
);
let mut out = Vec::new();
let mut n = Negotiator::new(&odb, &tip, AckMode::Single);
n.have(unknown, &mut out).unwrap();
n.flush(&mut out).unwrap();
n.have(ids[1], &mut out).unwrap();
n.have(ids[0], &mut out).unwrap();
n.flush(&mut out).unwrap();
n.done(&mut out).unwrap();
assert_eq!(lines(&out), ["NAK".to_string(), alloc::format!("ACK {b}")]);
let mut out = Vec::new();
Negotiator::new(&odb, &tip, AckMode::Detailed)
.done(&mut out)
.unwrap();
assert_eq!(lines(&out), ["NAK"]);
}
#[test]
fn plan_deepen_since_and_not() {
let (odb, ids) = chain(6);
struct Refs(ObjectId);
impl RefView for Refs {
fn resolve(&self, name: &str) -> Option<ObjectId> {
(name == "old").then_some(self.0)
}
fn tags(&self) -> Vec<ObjectId> {
Vec::new()
}
}
let refs = Refs(ids[2]);
let run = |deepen: Deepen| {
plan(
&odb,
&refs,
&Want {
wants: &[ids[5]],
haves: &[],
client_shallow: &[],
deepen: &deepen,
},
)
};
let p = run(Deepen {
since: Some(3),
..Default::default()
})
.unwrap();
assert_eq!(p.shallow, [ids[3]]);
assert!(p.objects.contains(&ids[3]) && !p.objects.contains(&ids[2]));
let p = run(Deepen {
not: alloc::vec!["old".into()],
..Default::default()
})
.unwrap();
assert_eq!(p.shallow, [ids[3]]);
assert!(
run(Deepen {
not: alloc::vec!["nope".into()],
..Default::default()
})
.is_err()
);
assert!(
run(Deepen {
since: Some(100),
..Default::default()
})
.is_err()
);
}
}