use std::collections::HashSet;
use std::io::{Read, Write};
use std::path::Path;
use crate::error::{Error, Result};
use crate::fetch_negotiator::SkippingNegotiator;
use crate::objects::ObjectId;
use crate::pkt_line;
use crate::protocol_v2;
use crate::refspec::{parse_fetch_refspec, RefspecItem};
use crate::transfer::{
classify_update, match_positive, open_odb, prune_tracking_refs, ref_excluded, refspecs_force,
FetchOptions, FetchOutcome, RefUpdate, UpdateMode,
};
use crate::transport::Connection;
pub trait Progress {
fn message(&mut self, _bytes: &[u8]) {}
}
pub struct NoProgress;
impl Progress for NoProgress {}
const INITIAL_FLUSH: usize = 16;
const PIPESAFE_FLUSH: usize = 32;
fn next_flush_count(count: usize) -> usize {
if count < PIPESAFE_FLUSH {
count * 2
} else {
count + PIPESAFE_FLUSH
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum AckKind {
Bare,
Common,
Continue,
Ready,
}
fn parse_ack(line: &str) -> Option<(ObjectId, AckKind)> {
if line == "NAK" {
return None;
}
let rest = line.strip_prefix("ACK ")?;
let hex = rest.split_whitespace().next()?;
let oid = ObjectId::from_hex(hex).ok()?;
let tail = rest.strip_prefix(hex).unwrap_or("").trim();
let kind = if tail.contains("continue") {
AckKind::Continue
} else if tail.contains("common") {
AckKind::Common
} else if tail.contains("ready") {
AckKind::Ready
} else {
AckKind::Bare
};
Some((oid, kind))
}
fn read_ack_round(reader: &mut dyn Read, negotiator: &mut SkippingNegotiator) -> Result<()> {
let mut reader = reader;
loop {
let Some(pkt) = pkt_line::read_packet(&mut reader)? else {
break;
};
match pkt {
pkt_line::Packet::Flush => break,
pkt_line::Packet::Data(ln) => {
let ln = ln.trim_end();
if ln == "NAK" {
break;
}
let Some((ack_oid, kind)) = parse_ack(ln) else {
break;
};
if kind == AckKind::Bare {
break;
}
let _ = negotiator.ack(ack_oid)?;
}
_ => {}
}
}
Ok(())
}
fn read_pkt_payload_raw(r: &mut dyn Read) -> std::io::Result<Option<Vec<u8>>> {
let mut len_buf = [0u8; 4];
match r.read_exact(&mut len_buf) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(e),
}
let len_str = std::str::from_utf8(&len_buf)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let len = usize::from_str_radix(len_str, 16)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
match len {
0..=2 => Ok(None),
n if n <= 4 => Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("invalid pkt-line length: {n}"),
)),
n => {
let payload_len = n - 4;
let mut buf = vec![0u8; payload_len];
r.read_exact(&mut buf)?;
Ok(Some(buf))
}
}
}
fn read_sideband_pack(
r: &mut dyn Read,
out: &mut Vec<u8>,
progress: &mut dyn Progress,
) -> Result<()> {
let mut seen_pack = false;
let mut pending: Vec<u8> = Vec::new();
loop {
let Some(payload) = read_pkt_payload_raw(r)? else {
break;
};
if payload.is_empty() {
continue;
}
match payload[0] {
1 => {
let data = &payload[1..];
if seen_pack {
out.extend_from_slice(data);
} else {
pending.extend_from_slice(data);
if let Some(pos) = pending.windows(4).position(|w| w == b"PACK") {
seen_pack = true;
out.extend_from_slice(&pending[pos..]);
pending.clear();
} else if pending.len() > 3 {
let keep_from = pending.len() - 3;
pending.drain(..keep_from);
}
}
}
2 => progress.message(&payload[1..]),
3 => {
return Err(Error::Message(format!(
"remote error: {}",
String::from_utf8_lossy(&payload[1..]).trim_end()
)));
}
_ => {
if !seen_pack && payload.starts_with(b"PACK") {
seen_pack = true;
out.extend_from_slice(&payload);
} else if seen_pack {
out.extend_from_slice(&payload);
}
}
}
}
Ok(())
}
fn peel_to_commit(repo: &crate::repo::Repository, oid: ObjectId) -> Option<ObjectId> {
let mut current = oid;
for _ in 0..16 {
let obj = repo.odb.read(¤t).ok()?;
match obj.kind {
crate::objects::ObjectKind::Commit => return Some(current),
crate::objects::ObjectKind::Tag => {
current = crate::objects::parse_tag(&obj.data).ok()?.object;
}
_ => return None,
}
}
None
}
#[derive(Default)]
pub(crate) struct ShallowUpdate {
pub(crate) shallow: Vec<ObjectId>,
pub(crate) unshallow: Vec<ObjectId>,
}
fn append_shallow_request_v0(
req: &mut Vec<u8>,
server_caps: &str,
local_shallow: &[ObjectId],
opts: &FetchOptions,
) -> Result<()> {
for oid in local_shallow {
pkt_line::write_line_to_vec(req, &format!("shallow {}", oid.to_hex()))?;
}
if opts.unshallow {
pkt_line::write_line_to_vec(req, &format!("deepen {}", crate::shallow::INFINITE_DEPTH))?;
} else if let Some(depth) = opts.depth.filter(|d| *d > 0) {
pkt_line::write_line_to_vec(req, &format!("deepen {depth}"))?;
}
if let Some(since) = opts.deepen_since.as_deref().filter(|s| !s.trim().is_empty()) {
if server_caps.contains("deepen-since") {
let value = crate::shallow::deepen_since_wire_value(since);
pkt_line::write_line_to_vec(req, &format!("deepen-since {value}"))?;
}
}
if server_caps.contains("deepen-not") {
for excl in &opts.deepen_not {
let excl = excl.trim();
if !excl.is_empty() {
pkt_line::write_line_to_vec(req, &format!("deepen-not {excl}"))?;
}
}
}
Ok(())
}
fn negotiate_pack(
local_git_dir: &Path,
conn: &mut dyn Connection,
wants: &[ObjectId],
opts: &FetchOptions,
local_shallow: &[ObjectId],
progress: &mut dyn Progress,
) -> Result<(Vec<u8>, ShallowUpdate)> {
let local_repo = crate::repo::Repository::open(local_git_dir, None)?;
let want_set: HashSet<ObjectId> = wants.iter().copied().collect();
let Some(first_want) = wants.first().copied() else {
return Ok((Vec::new(), ShallowUpdate::default()));
};
let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
let caps = " multi_ack_detailed side-band-64k thin-pack no-progress include-tag ofs-delta agent=grit";
let advertised: Vec<(String, ObjectId)> = conn.advertised_refs().to_vec();
let server_caps: String = conn.capabilities().join(" ");
let mut req: Vec<u8> = Vec::new();
let w0 = format!("want {}{}", first_want.to_hex(), caps);
pkt_line::write_line_to_vec(&mut req, &w0)?;
for w in wants.iter().skip(1) {
pkt_line::write_line_to_vec(&mut req, &format!("want {}", w.to_hex()))?;
}
if wants.len() == 1 && !shallow_request {
pkt_line::write_line_to_vec(&mut req, &format!("want {}", first_want.to_hex()))?;
}
append_shallow_request_v0(&mut req, &server_caps, local_shallow, opts)?;
req.extend_from_slice(b"0000");
conn.writer().write_all(&req)?;
conn.writer().flush()?;
let mut negotiator = SkippingNegotiator::new(local_repo);
let mut tips: Vec<ObjectId> = Vec::new();
let mut seen_tip: HashSet<ObjectId> = HashSet::new();
for prefix in ["refs/heads/", "refs/tags/"] {
if let Ok(entries) = crate::refs::list_refs(local_git_dir, prefix) {
for (_, oid) in entries {
if let Some(c) = peel_to_commit(negotiator.repo(), oid) {
if !want_set.contains(&c) && seen_tip.insert(c) {
tips.push(c);
}
}
}
}
}
if let Ok(h) = crate::refs::resolve_ref(local_git_dir, "HEAD") {
if let Some(c) = peel_to_commit(negotiator.repo(), h) {
if !want_set.contains(&c) && seen_tip.insert(c) {
tips.push(c);
}
}
}
tips.sort_by_key(ObjectId::to_hex);
if !shallow_request {
for t in tips {
negotiator.add_tip(t)?;
}
for (_, oid) in &advertised {
if want_set.contains(oid) {
continue;
}
if let Some(c) = peel_to_commit(negotiator.repo(), *oid) {
negotiator.known_common(c)?;
}
}
}
let mut shallow_update = ShallowUpdate::default();
if shallow_request {
let (sh, unsh) = crate::shallow::read_shallow_info_section(&mut conn.reader())?;
shallow_update.shallow = sh;
shallow_update.unshallow = unsh;
}
let mut count: usize = 0;
let mut flush_at: usize = INITIAL_FLUSH;
let mut pending: Vec<u8> = Vec::new();
let mut flushes: i32 = 0;
while let Some(oid) = negotiator.next_have()? {
pkt_line::write_line_to_vec(&mut pending, &format!("have {}", oid.to_hex()))?;
count += 1;
if flush_at <= count {
pending.extend_from_slice(b"0000");
conn.writer().write_all(&pending)?;
conn.writer().flush()?;
pending.clear();
flush_at = next_flush_count(count);
flushes += 1;
if count == INITIAL_FLUSH {
continue;
}
read_ack_round(conn.reader(), &mut negotiator)?;
flushes -= 1;
}
}
if !pending.is_empty() {
pending.extend_from_slice(b"0000");
conn.writer().write_all(&pending)?;
conn.writer().flush()?;
flushes += 1;
}
while flushes > 0 {
read_ack_round(conn.reader(), &mut negotiator)?;
flushes -= 1;
}
let mut tail = Vec::new();
pkt_line::write_line_to_vec(&mut tail, "done")?;
conn.writer().write_all(&tail)?;
conn.writer().flush()?;
match pkt_line::read_packet(&mut conn.reader())? {
None => return Err(Error::Message("unexpected EOF after done".to_owned())),
Some(pkt_line::Packet::Flush) => {
return Err(Error::Message("unexpected flush after done".to_owned()))
}
Some(pkt_line::Packet::Data(ln)) => {
let ln = ln.trim_end();
if ln != "NAK" {
if let Some((ack_oid, kind)) = parse_ack(ln) {
if kind != AckKind::Bare {
let _ = negotiator.ack(ack_oid)?;
}
} else if let Some(msg) = ln.strip_prefix("ERR ") {
return Err(Error::Message(format!("remote error: {}", msg.trim_end())));
}
}
}
Some(_) => {}
}
let mut pack = Vec::new();
read_sideband_pack(conn.reader(), &mut pack, progress)?;
Ok((pack, shallow_update))
}
pub(crate) fn v2_object_format(server_caps: &[String], local_odb: &crate::odb::Odb) -> String {
for c in server_caps {
if let Some(fmt) = c.strip_prefix("object-format=") {
let f = fmt.trim();
if !f.is_empty() {
return f.to_ascii_lowercase();
}
}
}
local_odb.hash_algo().name().to_owned()
}
fn v2_ref_prefixes_from_refspecs(refspecs: &[String]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let push_unique = |out: &mut Vec<String>, value: &str| {
if !out.iter().any(|v| v == value) {
out.push(value.to_owned());
}
};
for spec in refspecs {
if spec.starts_with('^') {
continue;
}
let raw = spec.strip_prefix('+').unwrap_or(spec.as_str());
let src = raw.split_once(':').map(|(s, _)| s).unwrap_or(raw).trim();
if src.is_empty() {
continue;
}
if src == "HEAD" {
push_unique(&mut out, "HEAD");
continue;
}
if let Some(star) = src.find('*') {
let prefix = &src[..star];
if prefix.is_empty() {
continue;
}
if prefix.starts_with("refs/") {
push_unique(&mut out, prefix);
} else {
push_unique(&mut out, &format!("refs/heads/{prefix}"));
}
continue;
}
if src.starts_with("refs/") {
push_unique(&mut out, src);
} else {
push_unique(&mut out, &format!("refs/heads/{src}"));
}
}
out
}
fn parse_ls_refs_v2_line(line: &str) -> Option<(String, ObjectId, Option<String>)> {
const SYM: &str = " symref-target:";
const PEEL: &str = " peeled:";
let (oid_hex, after_oid) = line.split_once(' ')?;
let oid = ObjectId::from_hex(oid_hex).ok()?;
let sym_at = after_oid.find(SYM);
let peel_at = after_oid.find(PEEL);
let name_end = match (sym_at, peel_at) {
(Some(a), Some(b)) => a.min(b),
(Some(a), None) => a,
(None, Some(b)) => b,
(None, None) => after_oid.len(),
};
let name = after_oid[..name_end].trim().to_owned();
if name.is_empty() {
return None;
}
let symref_target = sym_at.map(|pos| {
let tail = &after_oid[pos + SYM.len()..];
let end = tail.find(' ').unwrap_or(tail.len());
tail[..end].to_owned()
});
Some((name, oid, symref_target))
}
fn v2_ls_refs(
conn: &mut dyn Connection,
server_caps: &[String],
local_odb: &crate::odb::Odb,
tags: crate::transfer::TagMode,
refspecs: &[String],
) -> Result<(Vec<(String, ObjectId)>, Option<String>)> {
let req = build_v2_ls_refs_request(server_caps, local_odb, tags, refspecs)?;
conn.writer().write_all(&req)?;
conn.writer().flush()?;
parse_v2_ls_refs_response(conn.reader())
}
pub(crate) fn build_v2_ls_refs_request(
server_caps: &[String],
local_odb: &crate::odb::Odb,
tags: crate::transfer::TagMode,
refspecs: &[String],
) -> Result<Vec<u8>> {
let object_format = v2_object_format(server_caps, local_odb);
let cap_echo = protocol_v2::cap_lines_for_command_request(server_caps);
let mut req: Vec<u8> = Vec::new();
pkt_line::write_line(&mut req, "command=ls-refs")?;
if cap_echo.iter().any(|c| c.starts_with("object-format=")) {
for line in &cap_echo {
pkt_line::write_line(&mut req, line)?;
}
} else {
for line in &cap_echo {
pkt_line::write_line(&mut req, line)?;
}
pkt_line::write_line(&mut req, &format!("object-format={object_format}"))?;
}
pkt_line::write_delim(&mut req)?;
pkt_line::write_line(&mut req, "symrefs")?;
pkt_line::write_line(&mut req, "peel")?;
pkt_line::write_line(&mut req, "ref-prefix HEAD")?;
let mut prefixes = v2_ref_prefixes_from_refspecs(refspecs);
if prefixes.is_empty() {
prefixes.push("refs/heads/".to_owned());
prefixes.push("refs/tags/".to_owned());
} else if tags != crate::transfer::TagMode::None
&& !prefixes.iter().any(|p| p == "refs/tags/")
{
prefixes.push("refs/tags/".to_owned());
}
for p in &prefixes {
pkt_line::write_line(&mut req, &format!("ref-prefix {p}"))?;
}
pkt_line::write_flush(&mut req)?;
Ok(req)
}
pub(crate) fn parse_v2_ls_refs_response(
reader: &mut dyn Read,
) -> Result<(Vec<(String, ObjectId)>, Option<String>)> {
let mut advertised: Vec<(String, ObjectId)> = Vec::new();
let mut head_symref: Option<String> = None;
let mut reader = reader;
loop {
match pkt_line::read_packet(&mut reader)? {
None | Some(pkt_line::Packet::Flush) | Some(pkt_line::Packet::Delim) => break,
Some(pkt_line::Packet::ResponseEnd) => break,
Some(pkt_line::Packet::Data(line)) => {
let line = line.trim_end_matches('\n');
if let Some(msg) = line.strip_prefix("ERR ") {
return Err(Error::Message(format!(
"remote error: {}",
msg.trim_end()
)));
}
let Some((name, oid, symref_target)) = parse_ls_refs_v2_line(line) else {
continue;
};
if name.contains("^{") || name.ends_with("^{}") {
continue;
}
if name == "HEAD" {
if let Some(t) = symref_target {
head_symref = Some(t);
}
continue;
}
if name.starts_with("refs/heads/")
|| name.starts_with("refs/tags/")
|| name.starts_with("refs/")
{
advertised.push((name, oid));
}
}
}
}
Ok((advertised, head_symref))
}
pub(crate) fn v2_local_haves(
local_git_dir: &Path,
wants: &[ObjectId],
) -> Result<Vec<ObjectId>> {
let want_set: HashSet<ObjectId> = wants.iter().copied().collect();
let local_repo = crate::repo::Repository::open(local_git_dir, None)?;
let mut negotiator = SkippingNegotiator::new(local_repo);
let mut tips: Vec<ObjectId> = Vec::new();
let mut seen_tip: HashSet<ObjectId> = HashSet::new();
for prefix in ["refs/heads/", "refs/tags/"] {
if let Ok(entries) = crate::refs::list_refs(local_git_dir, prefix) {
for (_, oid) in entries {
if let Some(c) = peel_to_commit(negotiator.repo(), oid) {
if !want_set.contains(&c) && seen_tip.insert(c) {
tips.push(c);
}
}
}
}
}
if let Ok(h) = crate::refs::resolve_ref(local_git_dir, "HEAD") {
if let Some(c) = peel_to_commit(negotiator.repo(), h) {
if !want_set.contains(&c) && seen_tip.insert(c) {
tips.push(c);
}
}
}
tips.sort_by_key(ObjectId::to_hex);
for t in tips {
negotiator.add_tip(t)?;
}
let mut haves: Vec<ObjectId> = Vec::new();
while let Some(oid) = negotiator.next_have()? {
haves.push(oid);
}
Ok(haves)
}
fn negotiate_pack_v2(
local_git_dir: &Path,
conn: &mut dyn Connection,
server_caps: &[String],
local_odb: &crate::odb::Odb,
wants: &[ObjectId],
deepen: &V2DeepenArgs,
progress: &mut dyn Progress,
) -> Result<(Vec<u8>, ShallowUpdate)> {
if wants.is_empty() {
return Ok((Vec::new(), ShallowUpdate::default()));
}
let object_format = v2_object_format(server_caps, local_odb);
let cap_echo = protocol_v2::cap_lines_for_command_request(server_caps);
let sideband_all = protocol_v2::fetch_supports_sideband_all(server_caps);
let shallow_request = deepen.is_shallow_request();
let haves = if shallow_request {
Vec::new()
} else {
v2_local_haves(local_git_dir, wants)?
};
let mut pack = Vec::new();
let mut shallow_update = ShallowUpdate::default();
if haves.is_empty() {
write_v2_fetch_request(
conn.writer(),
&object_format,
&cap_echo,
wants,
&[],
sideband_all,
deepen,
true,
)?;
read_v2_fetch_pack_response(conn.reader(), &mut pack, &mut shallow_update, progress)?;
return Ok((pack, shallow_update));
}
let first_batch = haves.len().min(INITIAL_FLUSH);
write_v2_fetch_request(
conn.writer(),
&object_format,
&cap_echo,
wants,
&haves[..first_batch],
sideband_all,
deepen,
false,
)?;
let ack = read_v2_acknowledgments(conn.reader())?;
match ack {
Some(round) if round.ready => {
read_v2_fetch_pack_response(conn.reader(), &mut pack, &mut shallow_update, progress)?;
}
None => {
read_v2_fetch_pack_response(conn.reader(), &mut pack, &mut shallow_update, progress)?;
}
Some(_) => {
write_v2_fetch_request(
conn.writer(),
&object_format,
&cap_echo,
wants,
&haves[first_batch..],
sideband_all,
deepen,
true,
)?;
read_v2_fetch_pack_response(conn.reader(), &mut pack, &mut shallow_update, progress)?;
}
}
Ok((pack, shallow_update))
}
#[derive(Clone, Default)]
pub(crate) struct V2DeepenArgs {
pub(crate) local_shallow: Vec<ObjectId>,
pub(crate) depth: Option<u32>,
pub(crate) deepen_since: Option<String>,
pub(crate) deepen_not: Vec<String>,
}
impl V2DeepenArgs {
pub(crate) fn from_opts(opts: &FetchOptions, local_shallow: &[ObjectId]) -> Self {
let depth = if opts.unshallow {
Some(crate::shallow::INFINITE_DEPTH)
} else {
opts.depth.filter(|d| *d > 0)
};
Self {
local_shallow: local_shallow.to_vec(),
depth,
deepen_since: opts
.deepen_since
.as_deref()
.filter(|s| !s.trim().is_empty())
.map(crate::shallow::deepen_since_wire_value),
deepen_not: opts
.deepen_not
.iter()
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty())
.collect(),
}
}
pub(crate) fn is_shallow_request(&self) -> bool {
self.depth.is_some()
|| self.deepen_since.is_some()
|| !self.deepen_not.is_empty()
|| !self.local_shallow.is_empty()
}
}
pub(crate) fn write_v2_fetch_request(
w: &mut dyn Write,
object_format: &str,
cap_echo: &[String],
wants: &[ObjectId],
haves: &[ObjectId],
sideband_all: bool,
deepen: &V2DeepenArgs,
send_done: bool,
) -> Result<()> {
let mut req: Vec<u8> = Vec::new();
pkt_line::write_line(&mut req, "command=fetch")?;
if cap_echo.iter().any(|c| c.starts_with("object-format=")) {
for line in cap_echo {
pkt_line::write_line(&mut req, line)?;
}
} else {
for line in cap_echo {
pkt_line::write_line(&mut req, line)?;
}
pkt_line::write_line(&mut req, &format!("object-format={object_format}"))?;
}
pkt_line::write_delim(&mut req)?;
pkt_line::write_line(&mut req, "thin-pack")?;
pkt_line::write_line(&mut req, "no-progress")?;
pkt_line::write_line(&mut req, "ofs-delta")?;
if sideband_all {
pkt_line::write_line(&mut req, "sideband-all")?;
}
pkt_line::write_line(&mut req, "include-tag")?;
for oid in &deepen.local_shallow {
pkt_line::write_line(&mut req, &format!("shallow {}", oid.to_hex()))?;
}
if let Some(depth) = deepen.depth {
pkt_line::write_line(&mut req, &format!("deepen {depth}"))?;
}
if let Some(since) = &deepen.deepen_since {
pkt_line::write_line(&mut req, &format!("deepen-since {since}"))?;
}
for excl in &deepen.deepen_not {
pkt_line::write_line(&mut req, &format!("deepen-not {excl}"))?;
}
for want in wants {
pkt_line::write_line(&mut req, &format!("want {}", want.to_hex()))?;
}
for have in haves {
pkt_line::write_line(&mut req, &format!("have {}", have.to_hex()))?;
}
if send_done {
pkt_line::write_line(&mut req, "done")?;
}
pkt_line::write_flush(&mut req)?;
w.write_all(&req)?;
w.flush()?;
Ok(())
}
pub(crate) struct V2AckRound {
pub(crate) ready: bool,
}
pub(crate) fn read_v2_acknowledgments(reader: &mut dyn Read) -> Result<Option<V2AckRound>> {
let mut reader = reader;
let hdr = match pkt_line::read_packet(&mut reader)? {
Some(pkt_line::Packet::Data(s)) => s,
Some(pkt_line::Packet::Flush) => return Ok(Some(V2AckRound { ready: false })),
None => return Ok(None),
Some(other) => {
return Err(Error::Message(format!(
"unexpected v2 fetch response: {other:?}"
)))
}
};
let hdr = hdr.trim_end();
if let Some(msg) = hdr.strip_prefix("ERR ") {
return Err(Error::Message(format!("remote error: {}", msg.trim_end())));
}
if hdr != "acknowledgments" {
return Err(Error::Message(format!(
"unexpected v2 fetch section before acknowledgments: {hdr}"
)));
}
let mut ready = false;
loop {
match pkt_line::read_packet(&mut reader)? {
Some(pkt_line::Packet::Data(ln)) => {
let ln = ln.trim_end();
if ln == "NAK" || ln.starts_with("ACK ") {
continue;
}
if ln == "ready" {
ready = true;
continue;
}
return Err(Error::Message(format!(
"unexpected acknowledgment line: '{ln}'"
)));
}
Some(pkt_line::Packet::Delim) | Some(pkt_line::Packet::Flush) | None => break,
Some(other) => {
return Err(Error::Message(format!(
"unexpected acknowledgments packet: {other:?}"
)))
}
}
}
Ok(Some(V2AckRound { ready }))
}
pub(crate) fn read_v2_fetch_pack_response(
reader: &mut dyn Read,
out: &mut Vec<u8>,
shallow_out: &mut ShallowUpdate,
progress: &mut dyn Progress,
) -> Result<()> {
loop {
let hdr = match pkt_line::read_packet(&mut &mut *reader)? {
Some(pkt_line::Packet::Data(s)) => s,
Some(pkt_line::Packet::Flush) | None => return Ok(()),
Some(pkt_line::Packet::Delim) => continue,
Some(other) => {
return Err(Error::Message(format!(
"unexpected v2 fetch response: {other:?}"
)))
}
};
let hdr = hdr.trim_end();
if let Some(msg) = hdr.strip_prefix("ERR ") {
return Err(Error::Message(format!("remote error: {}", msg.trim_end())));
}
match hdr {
"shallow-info" => {
let (sh, unsh) = crate::shallow::read_shallow_info_section(&mut *reader)?;
shallow_out.shallow.extend(sh);
shallow_out.unshallow.extend(unsh);
}
"acknowledgments" | "wanted-refs" | "packfile-uris" => {
skip_v2_section_until_boundary(&mut *reader)?;
}
"packfile" => {
read_sideband_pack(&mut *reader, out, progress)?;
return Ok(());
}
other => {
return Err(Error::Message(format!(
"unexpected v2 fetch section: {other}"
)))
}
}
}
}
fn skip_v2_section_until_boundary(reader: &mut dyn Read) -> Result<()> {
loop {
match pkt_line::read_packet(&mut &mut *reader)? {
None | Some(pkt_line::Packet::Flush) | Some(pkt_line::Packet::Delim) => return Ok(()),
Some(pkt_line::Packet::ResponseEnd) => return Ok(()),
Some(pkt_line::Packet::Data(_)) => {}
}
}
}
pub fn fetch_remote(
local_git_dir: &Path,
conn: &mut dyn Connection,
opts: &FetchOptions,
progress: &mut dyn Progress,
) -> Result<FetchOutcome> {
let local_odb = open_odb(local_git_dir);
let (remote_refs, default_branch, v2_caps): (
Vec<(String, ObjectId)>,
Option<String>,
Option<Vec<String>>,
) = if conn.protocol_version() >= 2 {
let caps: Vec<String> = conn.capabilities().to_vec();
let (refs, head_symref) =
v2_ls_refs(conn, &caps, &local_odb, opts.tags, &opts.refspecs)?;
let default_branch = head_symref.map(|t| {
t.strip_prefix("refs/heads/")
.unwrap_or(&t)
.to_owned()
});
(refs, default_branch, Some(caps))
} else {
let default_branch = conn.head_symref().map(|t| {
t.strip_prefix("refs/heads/")
.unwrap_or(t)
.to_owned()
});
let remote_refs: Vec<(String, ObjectId)> = conn
.advertised_refs()
.iter()
.filter(|(n, _)| n != "HEAD" && !n.ends_with("^{}"))
.cloned()
.collect();
(remote_refs, default_branch, None)
};
let mut positive: Vec<RefspecItem> = Vec::new();
let mut negatives: Vec<RefspecItem> = Vec::new();
for spec in &opts.refspecs {
let item = parse_fetch_refspec(spec)
.map_err(|e| Error::Message(format!("invalid refspec '{spec}': {e}")))?;
if item.negative {
negatives.push(item);
} else {
positive.push(item);
}
}
for spec in &opts.negative_refspecs {
let item = parse_fetch_refspec(spec)
.map_err(|e| Error::Message(format!("invalid negative refspec '{spec}': {e}")))?;
negatives.push(item);
}
let mut matched: Vec<crate::transfer::MatchedRef> = Vec::new();
let mut matched_oids: HashSet<ObjectId> = HashSet::new();
let mut seen_remote_ref: HashSet<String> = HashSet::new();
for (name, oid) in &remote_refs {
if ref_excluded(name, &negatives) {
continue;
}
if let Some(local_ref) = match_positive(name, &positive) {
if seen_remote_ref.insert(name.clone()) {
matched_oids.insert(*oid);
matched.push(crate::transfer::MatchedRef {
remote_ref: name.clone(),
local_ref,
oid: *oid,
force: refspecs_force(name, &positive),
is_tag: name.starts_with("refs/tags/"),
});
}
}
}
let following_only = add_wire_tags(
opts.tags,
&remote_refs,
&negatives,
&mut matched,
&mut matched_oids,
&mut seen_remote_ref,
);
let local_shallow = crate::shallow::load_shallow_oids(local_git_dir)?;
let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
let wants: Vec<ObjectId> = if shallow_request {
matched_oids
.iter()
.copied()
.filter(|oid| !following_only.contains(oid))
.collect()
} else {
matched_oids
.iter()
.copied()
.filter(|oid| !following_only.contains(oid) && !local_odb.exists(oid))
.collect()
};
let mut shallow_update = ShallowUpdate::default();
if !wants.is_empty() && !opts.dry_run {
let (pack, su) = if let Some(caps) = v2_caps.as_ref() {
let deepen = V2DeepenArgs::from_opts(opts, &local_shallow);
negotiate_pack_v2(local_git_dir, conn, caps, &local_odb, &wants, &deepen, progress)?
} else {
negotiate_pack(local_git_dir, conn, &wants, opts, &local_shallow, progress)?
};
shallow_update = su;
if !pack.is_empty() {
let mut cursor = std::io::Cursor::new(pack);
crate::unpack_objects::unpack_objects(
&mut cursor,
&local_odb,
&crate::unpack_objects::UnpackOptions {
quiet: true,
..Default::default()
},
)?;
}
}
if !opts.dry_run {
crate::shallow::apply_shallow_updates(
local_git_dir,
&shallow_update.shallow,
&shallow_update.unshallow,
)?;
}
if v2_caps.is_some() {
conn.finish_send();
}
if opts.tags == crate::transfer::TagMode::Following {
retain_following_tags(&local_odb, &mut matched, &matched_oids);
}
let local_repo = if opts.dry_run {
None
} else {
crate::repo::Repository::open(local_git_dir, None).ok()
};
let mut updates: Vec<RefUpdate> = Vec::new();
if opts.prune {
prune_tracking_refs(
local_git_dir,
&positive,
&remote_refs,
opts.dry_run,
&mut updates,
)?;
}
for m in &matched {
let Some(local_ref) = &m.local_ref else {
updates.push(RefUpdate {
remote_ref: m.remote_ref.clone(),
local_ref: None,
old_oid: None,
new_oid: Some(m.oid),
mode: UpdateMode::NoChangeNeeded,
note: Some("not stored (empty destination)".to_owned()),
});
continue;
};
let old = crate::refs::resolve_ref(local_git_dir, local_ref).ok();
let mode = classify_update(old.as_ref(), &m.oid, m.force, m.is_tag, local_repo.as_ref());
let write = matches!(
mode,
UpdateMode::New | UpdateMode::FastForward | UpdateMode::Forced
);
if write && !opts.dry_run {
crate::refs::write_ref(local_git_dir, local_ref, &m.oid)?;
}
updates.push(RefUpdate {
remote_ref: m.remote_ref.clone(),
local_ref: Some(local_ref.clone()),
old_oid: old,
new_oid: Some(m.oid),
mode,
note: None,
});
}
Ok(FetchOutcome {
updates,
default_branch,
new_shallow: shallow_update.shallow,
new_unshallow: shallow_update.unshallow,
})
}
fn add_wire_tags(
mode: crate::transfer::TagMode,
remote_refs: &[(String, ObjectId)],
negatives: &[RefspecItem],
matched: &mut Vec<crate::transfer::MatchedRef>,
matched_oids: &mut HashSet<ObjectId>,
seen_remote_ref: &mut HashSet<String>,
) -> HashSet<ObjectId> {
let mut following_only: HashSet<ObjectId> = HashSet::new();
if mode == crate::transfer::TagMode::None {
return following_only;
}
for (name, oid) in remote_refs {
if !name.starts_with("refs/tags/") {
continue;
}
if seen_remote_ref.contains(name) || ref_excluded(name, negatives) {
continue;
}
seen_remote_ref.insert(name.clone());
matched_oids.insert(*oid);
if mode == crate::transfer::TagMode::Following {
following_only.insert(*oid);
}
matched.push(crate::transfer::MatchedRef {
remote_ref: name.clone(),
local_ref: Some(name.clone()),
oid: *oid,
force: false,
is_tag: true,
});
}
following_only
}
fn retain_following_tags(
local_odb: &crate::odb::Odb,
matched: &mut Vec<crate::transfer::MatchedRef>,
matched_oids: &HashSet<ObjectId>,
) {
let roots: Vec<ObjectId> = matched
.iter()
.filter(|m| !m.is_tag)
.map(|m| m.oid)
.collect();
let closure = reachable_closure(local_odb, &roots);
matched.retain(|m| {
if !m.is_tag {
return true;
}
let peeled = peel_tag_target(local_odb, m.oid);
let have = local_odb.exists(&m.oid);
have && (closure.contains(&m.oid)
|| closure.contains(&peeled)
|| matched_oids.contains(&peeled))
});
}
fn peel_tag_target(odb: &crate::odb::Odb, oid: ObjectId) -> ObjectId {
let mut current = oid;
for _ in 0..16 {
let Ok(obj) = odb.read(¤t) else {
return current;
};
if obj.kind != crate::objects::ObjectKind::Tag {
return current;
}
match crate::objects::parse_tag(&obj.data) {
Ok(t) => current = t.object,
Err(_) => return current,
}
}
current
}
fn reachable_closure(odb: &crate::odb::Odb, roots: &[ObjectId]) -> HashSet<ObjectId> {
use crate::objects::{parse_commit, parse_tag, parse_tree, ObjectKind};
let mut seen: HashSet<ObjectId> = HashSet::new();
let mut stack: Vec<ObjectId> = roots.to_vec();
while let Some(oid) = stack.pop() {
if !seen.insert(oid) {
continue;
}
let Ok(obj) = odb.read(&oid) else {
continue;
};
match obj.kind {
ObjectKind::Commit => {
if let Ok(c) = parse_commit(&obj.data) {
stack.push(c.tree);
for p in c.parents {
stack.push(p);
}
}
}
ObjectKind::Tree => {
if let Ok(entries) = parse_tree(&obj.data) {
for e in entries {
stack.push(e.oid);
}
}
}
ObjectKind::Tag => {
if let Ok(t) = parse_tag(&obj.data) {
stack.push(t.object);
}
}
ObjectKind::Blob => {}
}
}
seen
}