use std::collections::HashSet;
use std::io::{Cursor, Read, Write};
use std::path::Path;
use crate::error::{Error, Result};
use crate::fetch::Progress;
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, TagMode, UpdateMode,
};
use crate::transport::{Advertisement, Connection, ConnectOptions, Service, Transport};
#[cfg(feature = "http-ureq")]
pub mod ureq_client;
pub trait HttpClient: Send + Sync {
fn get(&self, url: &str, git_protocol: Option<&str>) -> Result<Vec<u8>>;
fn post(
&self,
url: &str,
content_type: &str,
accept: &str,
body: &[u8],
git_protocol: Option<&str>,
) -> Result<Vec<u8>>;
fn git_protocol_header(&self) -> Option<&str> {
None
}
fn smart_http_enabled(&self) -> bool {
true
}
}
impl<C: HttpClient> HttpClient for std::sync::Arc<C> {
fn get(&self, url: &str, git_protocol: Option<&str>) -> Result<Vec<u8>> {
(**self).get(url, git_protocol)
}
fn post(
&self,
url: &str,
content_type: &str,
accept: &str,
body: &[u8],
git_protocol: Option<&str>,
) -> Result<Vec<u8>> {
(**self).post(url, content_type, accept, body, git_protocol)
}
fn git_protocol_header(&self) -> Option<&str> {
(**self).git_protocol_header()
}
fn smart_http_enabled(&self) -> bool {
(**self).smart_http_enabled()
}
}
const UPLOAD_PACK: &str = "git-upload-pack";
fn strip_service_advertisement(body: &[u8]) -> Result<&[u8]> {
let mut cur = Cursor::new(body);
let start = cur.position();
match pkt_line::read_packet(&mut cur)? {
Some(pkt_line::Packet::Data(line)) if line.starts_with("# service=") => {
match pkt_line::read_packet(&mut cur)? {
Some(pkt_line::Packet::Flush) | None => {}
_ => {
return Ok(body);
}
}
let pos = cur.position() as usize;
Ok(&body[pos..])
}
_ => {
cur.set_position(start);
Ok(body)
}
}
}
#[derive(Clone, Debug)]
struct AdvRef {
name: String,
oid: ObjectId,
}
struct Discovery {
protocol_version: u8,
refs: Vec<AdvRef>,
caps: HashSet<String>,
head_symref: Option<String>,
object_format: String,
}
fn parse_advertisement(body: &[u8]) -> Result<Discovery> {
let mut cur = Cursor::new(body);
let first = match pkt_line::read_packet(&mut cur)? {
None | Some(pkt_line::Packet::Flush) => {
return Ok(Discovery {
protocol_version: 0,
refs: Vec::new(),
caps: HashSet::new(),
head_symref: None,
object_format: "sha1".to_owned(),
});
}
Some(pkt_line::Packet::Data(s)) => s,
Some(other) => {
return Err(Error::Message(format!(
"unexpected first advertisement packet: {other:?}"
)))
}
};
if first.trim_end() == "version 2" {
let mut caps = HashSet::new();
loop {
match pkt_line::read_packet(&mut cur)? {
None | Some(pkt_line::Packet::Flush) => break,
Some(pkt_line::Packet::Data(s)) => {
caps.insert(s.trim_end().to_owned());
}
Some(_) => break,
}
}
let object_format = caps
.iter()
.find_map(|c| c.strip_prefix("object-format="))
.unwrap_or("sha1")
.to_owned();
return Ok(Discovery {
protocol_version: 2,
refs: Vec::new(),
caps,
head_symref: None,
object_format,
});
}
cur.set_position(0);
let mut refs = Vec::new();
let mut caps: HashSet<String> = HashSet::new();
let mut head_symref = None;
let mut first_ref_line = true;
loop {
match pkt_line::read_packet(&mut cur)? {
None | Some(pkt_line::Packet::Flush) => break,
Some(pkt_line::Packet::Data(line)) => {
let line = line.trim_end_matches('\n');
if line.starts_with("version ") {
continue;
}
if line.starts_with("shallow ") || line.starts_with("unshallow ") {
continue;
}
let (payload, cap_part) = match line.split_once('\0') {
Some((p, c)) => (p.trim(), Some(c)),
None => (line.trim(), None),
};
let Some((oid_hex, refname)) =
payload.split_once('\t').or_else(|| payload.split_once(' '))
else {
continue;
};
let oid_hex = oid_hex.trim();
let refname = refname.trim();
if first_ref_line {
if let Some(raw_caps) = cap_part {
for cap in raw_caps.split_whitespace() {
if let Some(target) = cap.strip_prefix("symref=HEAD:") {
head_symref = Some(target.to_owned());
}
caps.insert(cap.to_owned());
}
}
first_ref_line = false;
}
if refname.is_empty() {
continue;
}
if oid_hex.bytes().all(|b| b == b'0') {
continue;
}
let oid = ObjectId::from_hex(oid_hex).map_err(|e| {
Error::Message(format!("bad oid in advertisement: {oid_hex}: {e}"))
})?;
refs.push(AdvRef {
name: refname.to_owned(),
oid,
});
}
Some(other) => {
return Err(Error::Message(format!(
"unexpected packet in advertisement: {other:?}"
)))
}
}
}
let object_format = caps
.iter()
.find_map(|c| c.strip_prefix("object-format="))
.unwrap_or("sha1")
.to_owned();
Ok(Discovery {
protocol_version: if caps.contains("version 1") { 1 } else { 0 },
refs,
caps,
head_symref,
object_format,
})
}
fn info_refs_url(repo_url: &str) -> String {
let base = repo_url.trim_end_matches('/');
let mut url = format!("{base}/info/refs");
url.push_str(if url.contains('?') { "&" } else { "?" });
url.push_str("service=");
url.push_str(UPLOAD_PACK);
url
}
fn upload_pack_url(repo_url: &str) -> String {
let base = repo_url.trim_end_matches('/');
format!("{base}/{UPLOAD_PACK}")
}
pub struct SmartHttpConnection {
repo_url: String,
adv_refs: Vec<(String, ObjectId)>,
caps: Vec<String>,
head_symref: Option<String>,
protocol_version: u8,
object_format: String,
service: Service,
empty_reader: Cursor<Vec<u8>>,
sink: Vec<u8>,
}
impl SmartHttpConnection {
#[must_use]
pub fn repo_url(&self) -> &str {
&self.repo_url
}
#[must_use]
pub fn object_format(&self) -> &str {
&self.object_format
}
#[must_use]
pub fn service(&self) -> Service {
self.service
}
}
impl Connection for SmartHttpConnection {
fn reader(&mut self) -> &mut dyn Read {
&mut self.empty_reader
}
fn writer(&mut self) -> &mut dyn Write {
&mut self.sink
}
fn advertised_refs(&self) -> &[(String, ObjectId)] {
&self.adv_refs
}
fn capabilities(&self) -> &[String] {
&self.caps
}
fn head_symref(&self) -> Option<&str> {
self.head_symref.as_deref()
}
fn protocol_version(&self) -> u8 {
self.protocol_version
}
}
pub struct SmartHttpTransport<C: HttpClient> {
client: C,
}
impl<C: HttpClient> SmartHttpTransport<C> {
pub fn new(client: C) -> Self {
Self { client }
}
pub fn client(&self) -> &C {
&self.client
}
pub fn push(
&self,
local_git_dir: &Path,
repo_url: &str,
refs: &[crate::transfer::PushRefSpec],
opts: &crate::transfer::PushOptions,
progress: &mut dyn Progress,
) -> Result<crate::transfer::PushOutcome> {
crate::push::push_http(&self.client, local_git_dir, repo_url, refs, opts, progress)
}
fn discover(
&self,
repo_url: &str,
_service: Service,
git_protocol: Option<&str>,
) -> Result<Discovery> {
let url = info_refs_url(repo_url);
let gp = git_protocol.or_else(|| self.client.git_protocol_header());
let body = self.client.get(&url, gp)?;
let stripped = strip_service_advertisement(&body)?;
parse_advertisement(stripped)
}
}
fn git_protocol_for_version(version: u8) -> Option<String> {
if version >= 1 {
Some(format!("version={version}"))
} else {
None
}
}
impl<C: HttpClient> Transport for SmartHttpTransport<C> {
fn connect(
&self,
url: &str,
service: Service,
opts: &ConnectOptions,
) -> Result<Box<dyn Connection>> {
crate::net_trace::net_trace!(
"http(s) discover {url} (service={}, request protocol v{})",
service.wire_name(),
opts.protocol_version
);
let gp = git_protocol_for_version(opts.protocol_version);
let disc = self.discover(url, service, gp.as_deref())?;
let adv_refs: Vec<(String, ObjectId)> = disc
.refs
.iter()
.filter(|r| r.name != "HEAD" && !r.name.ends_with("^{}"))
.map(|r| (r.name.clone(), r.oid))
.collect();
let caps: Vec<String> = disc.caps.iter().cloned().collect();
crate::net_trace::net_trace!(
"http(s) discovered: protocol v{}, {} ref(s) advertised",
disc.protocol_version,
adv_refs.len()
);
Ok(Box::new(SmartHttpConnection {
repo_url: url.to_owned(),
adv_refs,
caps,
head_symref: disc.head_symref,
protocol_version: disc.protocol_version,
object_format: disc.object_format,
service,
empty_reader: Cursor::new(Vec::new()),
sink: Vec::new(),
}))
}
}
fn read_pkt_payload(r: &mut impl 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 mut buf = vec![0u8; n - 4];
r.read_exact(&mut buf)?;
Ok(Some(buf))
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum AckKind {
Bare,
Common,
Continue,
Ready,
}
struct Ack {
oid: ObjectId,
kind: AckKind,
}
fn parse_ack(line: &str) -> Option<Ack> {
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(Ack { oid, kind })
}
struct RoundResult {
acks: Vec<Ack>,
got_pack: bool,
shallow: Vec<ObjectId>,
unshallow: Vec<ObjectId>,
}
fn read_sideband_pack(
r: &mut impl Read,
out: &mut Vec<u8>,
progress: &mut dyn Progress,
) -> Result<()> {
let mut seen_pack = false;
let mut pending: Vec<u8> = Vec::new();
loop {
let mut len_buf = [0u8; 4];
match r.read_exact(&mut len_buf) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(e.into()),
}
let len_str = std::str::from_utf8(&len_buf)
.map_err(|_| Error::Message("bad pkt length".to_owned()))?;
let len = usize::from_str_radix(len_str, 16)
.map_err(|_| Error::Message("bad pkt length".to_owned()))?;
match len {
0 => {
if seen_pack {
break;
}
continue;
}
1 | 2 => continue,
n if n <= 4 => {
return Err(Error::Message(format!(
"invalid pkt-line length in side-band stream: {n}"
)))
}
_ => {}
}
let mut payload = vec![0u8; len - 4];
r.read_exact(&mut payload)?;
if payload.is_empty() {
continue;
}
match payload[0] {
1 => append_pack_data(&payload[1..], out, &mut pending, &mut seen_pack),
2 => progress.message(&payload[1..]),
3 => {
return Err(Error::Message(format!(
"remote error: {}",
String::from_utf8_lossy(&payload[1..]).trim_end()
)))
}
_ => append_pack_data(&payload, out, &mut pending, &mut seen_pack),
}
}
Ok(())
}
fn append_pack_data(data: &[u8], out: &mut Vec<u8>, pending: &mut Vec<u8>, seen_pack: &mut bool) {
if *seen_pack {
out.extend_from_slice(data);
return;
}
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);
}
}
fn read_stateless_response(
resp: &[u8],
sideband: bool,
expect_shallow: bool,
pack_buf: &mut Vec<u8>,
progress: &mut dyn Progress,
) -> Result<RoundResult> {
let mut cur = Cursor::new(resp);
let mut acks = Vec::new();
let mut got_pack = false;
let mut shallow = Vec::new();
let mut unshallow = Vec::new();
if expect_shallow {
loop {
let start = cur.position() as usize;
match pkt_line::read_packet(&mut cur)? {
None | Some(pkt_line::Packet::Flush) => break,
Some(pkt_line::Packet::Data(line)) => {
let line = line.trim_end_matches('\n');
if let Some(rest) = line.strip_prefix("shallow ") {
if let Ok(oid) = ObjectId::from_hex(rest.trim()) {
shallow.push(oid);
}
} else if let Some(rest) = line.strip_prefix("unshallow ") {
if let Ok(oid) = ObjectId::from_hex(rest.trim()) {
unshallow.push(oid);
}
} else {
cur.set_position(start as u64);
break;
}
}
Some(_) => break,
}
}
}
loop {
let start = cur.position() as usize;
let Some(payload) = read_pkt_payload(&mut cur)? else {
break;
};
if payload.is_empty() {
continue;
}
let is_pack = (sideband
&& payload.first() == Some(&1)
&& payload.get(1..5) == Some(b"PACK"))
|| payload.starts_with(b"PACK");
if is_pack {
got_pack = true;
cur.set_position(start as u64);
if sideband {
read_sideband_pack(&mut cur, pack_buf, progress)?;
} else {
pack_buf.extend_from_slice(&resp[start..]);
}
break;
}
let text = String::from_utf8_lossy(&payload);
let line = text.trim_end_matches('\n');
if let Some(err) = line.strip_prefix("ERR ") {
return Err(Error::Message(format!("remote upload-pack error: {err}")));
}
if line == "NAK" {
continue;
}
if let Some(ack) = parse_ack(line) {
acks.push(ack);
}
}
Ok(RoundResult {
acks,
got_pack,
shallow,
unshallow,
})
}
fn build_fetch_caps(caps: &HashSet<String>) -> String {
let mut enabled = Vec::new();
let multi_ack_detailed = caps.contains("multi_ack_detailed");
if multi_ack_detailed {
enabled.push("multi_ack_detailed");
}
if multi_ack_detailed && caps.contains("no-done") {
enabled.push("no-done");
}
for want in [
"side-band-64k",
"thin-pack",
"no-progress",
"include-tag",
"ofs-delta",
] {
if caps.contains(want) {
enabled.push(want);
}
}
if enabled.is_empty() {
String::new()
} else {
format!(" {}", enabled.join(" "))
}
}
fn next_flush(count: usize) -> usize {
const LARGE_FLUSH: usize = 16384;
if count < LARGE_FLUSH {
count * 2
} else {
count * 11 / 10
}
}
fn append_shallow_request_v0_http(
req: &mut Vec<u8>,
caps: &HashSet<String>,
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 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 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_http(
client: &dyn HttpClient,
local_git_dir: &Path,
repo_url: &str,
caps: &HashSet<String>,
advertised: &[AdvRef],
wants: &[ObjectId],
opts: &FetchOptions,
local_shallow: &[ObjectId],
progress: &mut dyn Progress,
) -> Result<(Vec<u8>, crate::fetch::ShallowUpdate)> {
let post_url = upload_pack_url(repo_url);
let content_type = format!("application/x-{UPLOAD_PACK}-request");
let accept = format!("application/x-{UPLOAD_PACK}-result");
let fetch_caps = build_fetch_caps(caps);
let sideband = caps.contains("side-band-64k");
let multi_ack_detailed = caps.contains("multi_ack_detailed");
let no_done = multi_ack_detailed && caps.contains("no-done");
let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
let want_set: HashSet<ObjectId> = wants.iter().copied().collect();
let mut state = Vec::new();
let first = wants[0];
pkt_line::write_line_to_vec(&mut state, &format!("want {}{}", first.to_hex(), fetch_caps))?;
for w in wants.iter().skip(1) {
pkt_line::write_line_to_vec(&mut state, &format!("want {}", w.to_hex()))?;
}
append_shallow_request_v0_http(&mut state, caps, local_shallow, opts)?;
pkt_line::write_flush(&mut state)?;
let mut shallow_update = crate::fetch::ShallowUpdate::default();
let local_repo = crate::repo::Repository::open(local_git_dir, None)?;
let mut negotiator = SkippingNegotiator::new(local_repo);
if !shallow_request {
for w in wants {
if negotiator.repo().odb.read(w).is_ok() {
negotiator.add_tip(*w)?;
}
}
let mut tips: Vec<ObjectId> = Vec::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 negotiator.repo().odb.read(&oid).is_ok() {
tips.push(oid);
}
}
}
}
if let Ok(h) = crate::refs::resolve_ref(local_git_dir, "HEAD") {
if negotiator.repo().odb.read(&h).is_ok() {
tips.push(h);
}
}
tips.sort_by_key(ObjectId::to_hex);
tips.dedup();
for t in tips {
if want_set.contains(&t) {
continue;
}
negotiator.add_tip(t)?;
}
for e in advertised {
if want_set.contains(&e.oid) {
continue;
}
if negotiator.repo().odb.read(&e.oid).is_ok() {
negotiator.known_common(e.oid)?;
}
}
}
let mut pack_buf: Vec<u8> = Vec::new();
let mut got_ready = false;
let mut got_pack = false;
let mut shallow_applied = false;
const INITIAL_FLUSH: usize = 16;
let mut count: usize = 0;
let mut flush_at: usize = INITIAL_FLUSH;
let mut round = Vec::new();
while let Some(oid) = negotiator.next_have()? {
pkt_line::write_line_to_vec(&mut round, &format!("have {}", oid.to_hex()))?;
count += 1;
if count < flush_at {
continue;
}
flush_at = next_flush(count);
let mut req = state.clone();
req.extend_from_slice(&round);
pkt_line::write_flush(&mut req)?;
round.clear();
let resp = client.post(&post_url, &content_type, &accept, &req, None)?;
let round_result =
read_stateless_response(&resp, sideband, shallow_request, &mut pack_buf, progress)?;
if shallow_request && !shallow_applied {
shallow_update.shallow.extend(round_result.shallow.iter().copied());
shallow_update.unshallow.extend(round_result.unshallow.iter().copied());
shallow_applied = true;
}
for ack in &round_result.acks {
if matches!(ack.kind, AckKind::Bare) {
continue;
}
let was_common = negotiator.ack(ack.oid)?;
if matches!(ack.kind, AckKind::Common) && !was_common {
pkt_line::write_line_to_vec(&mut state, &format!("have {}", ack.oid.to_hex()))?;
}
if matches!(ack.kind, AckKind::Ready) {
got_ready = true;
}
}
if round_result.got_pack {
got_pack = true;
break;
}
if got_ready {
break;
}
}
if !(got_pack || got_ready && no_done) {
let mut req = state.clone();
pkt_line::write_line_to_vec(&mut req, "done")?;
pkt_line::write_flush(&mut req)?;
let resp = client.post(&post_url, &content_type, &accept, &req, None)?;
let round_result =
read_stateless_response(&resp, sideband, shallow_request, &mut pack_buf, progress)?;
if shallow_request && !shallow_applied {
shallow_update.shallow.extend(round_result.shallow);
shallow_update.unshallow.extend(round_result.unshallow);
}
}
Ok((pack_buf, shallow_update))
}
struct MatchPlan {
matched: Vec<crate::transfer::MatchedRef>,
wants: HashSet<ObjectId>,
seen: HashSet<String>,
}
fn match_refspecs(
remote_refs: &[(String, ObjectId)],
positive: &[RefspecItem],
negatives: &[RefspecItem],
) -> MatchPlan {
let mut matched: Vec<crate::transfer::MatchedRef> = Vec::new();
let mut wants: HashSet<ObjectId> = HashSet::new();
let mut seen: 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.insert(name.clone()) {
wants.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/"),
});
}
}
}
MatchPlan {
matched,
wants,
seen,
}
}
pub fn http_fetch(
client: &dyn HttpClient,
local_git_dir: &Path,
repo_url: &str,
opts: &FetchOptions,
progress: &mut dyn Progress,
) -> Result<FetchOutcome> {
use crate::net_trace::net_trace;
net_trace!(
"http_fetch: begin — {} ({} refspec(s), tags={:?})",
repo_url,
opts.refspecs.len(),
opts.tags
);
let disc = {
let url = info_refs_url(repo_url);
let body = client.get(&url, client.git_protocol_header())?;
let stripped = strip_service_advertisement(&body)?;
parse_advertisement(stripped)?
};
net_trace!(
"http_fetch: discovered protocol v{}, {} ref(s)",
disc.protocol_version,
disc.refs.len()
);
if disc.protocol_version >= 2 {
net_trace!("http_fetch: delegating to v2 stateless fetch");
return http_fetch_v2(client, local_git_dir, repo_url, &disc, opts, progress);
}
let local_odb = open_odb(local_git_dir);
let default_branch = disc
.head_symref
.as_deref()
.map(|t| t.strip_prefix("refs/heads/").unwrap_or(t).to_owned());
let remote_refs: Vec<(String, ObjectId)> = disc
.refs
.iter()
.filter(|r| r.name != "HEAD" && !r.name.ends_with("^{}"))
.map(|r| (r.name.clone(), r.oid))
.collect();
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 MatchPlan {
mut matched,
mut wants,
mut seen,
} = match_refspecs(&remote_refs, &positive, &negatives);
if opts.tags != TagMode::None {
for (name, oid) in &remote_refs {
if !name.starts_with("refs/tags/") {
continue;
}
if seen.contains(name) || ref_excluded(name, &negatives) {
continue;
}
seen.insert(name.clone());
wants.insert(*oid);
matched.push(crate::transfer::MatchedRef {
remote_ref: name.clone(),
local_ref: Some(name.clone()),
oid: *oid,
force: false,
is_tag: true,
});
}
}
let local_shallow = crate::shallow::load_shallow_oids(local_git_dir)?;
let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
let need: Vec<ObjectId> = if shallow_request {
wants.iter().copied().collect()
} else {
wants
.iter()
.copied()
.filter(|oid| !local_odb.exists(oid))
.collect()
};
let mut shallow_update = crate::fetch::ShallowUpdate::default();
if !need.is_empty() && !opts.dry_run {
let (pack, su) = negotiate_pack_http(
client,
local_git_dir,
repo_url,
&disc.caps,
&disc.refs,
&need,
opts,
&local_shallow,
progress,
)?;
shallow_update = su;
if !pack.is_empty() {
if pack.len() < 12 || &pack[0..4] != b"PACK" {
return Err(Error::Message(
"did not receive a valid pack from HTTP fetch".to_owned(),
));
}
let mut cursor = 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 opts.tags == TagMode::Following {
retain_following_tags(&local_odb, &mut matched, &wants);
}
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,
});
}
net_trace!("http_fetch: done — {} ref update(s)", updates.len());
Ok(FetchOutcome {
updates,
default_branch,
new_shallow: shallow_update.shallow,
new_unshallow: shallow_update.unshallow,
})
}
fn http_fetch_v2(
client: &dyn HttpClient,
local_git_dir: &Path,
repo_url: &str,
disc: &Discovery,
opts: &FetchOptions,
progress: &mut dyn Progress,
) -> Result<FetchOutcome> {
let local_odb = open_odb(local_git_dir);
let server_caps: Vec<String> = disc.caps.iter().cloned().collect();
let post_url = upload_pack_url(repo_url);
let content_type = format!("application/x-{UPLOAD_PACK}-request");
let accept = format!("application/x-{UPLOAD_PACK}-result");
let git_protocol = "version=2";
let (remote_refs, head_symref) = {
let req =
crate::fetch::build_v2_ls_refs_request(&server_caps, &local_odb, opts.tags, &opts.refspecs)?;
let resp = client.post(&post_url, &content_type, &accept, &req, Some(git_protocol))?;
let mut cur = Cursor::new(resp);
crate::fetch::parse_v2_ls_refs_response(&mut cur)?
};
let default_branch = head_symref
.as_deref()
.map(|t| t.strip_prefix("refs/heads/").unwrap_or(t).to_owned());
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 MatchPlan {
mut matched,
mut wants,
mut seen,
} = match_refspecs(&remote_refs, &positive, &negatives);
if opts.tags != TagMode::None {
for (name, oid) in &remote_refs {
if !name.starts_with("refs/tags/") {
continue;
}
if seen.contains(name) || ref_excluded(name, &negatives) {
continue;
}
seen.insert(name.clone());
wants.insert(*oid);
matched.push(crate::transfer::MatchedRef {
remote_ref: name.clone(),
local_ref: Some(name.clone()),
oid: *oid,
force: false,
is_tag: true,
});
}
}
let local_shallow = crate::shallow::load_shallow_oids(local_git_dir)?;
let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
let need: Vec<ObjectId> = if shallow_request {
wants.iter().copied().collect()
} else {
wants
.iter()
.copied()
.filter(|oid| !local_odb.exists(oid))
.collect()
};
let mut shallow_update = crate::fetch::ShallowUpdate::default();
if !need.is_empty() && !opts.dry_run {
let deepen = crate::fetch::V2DeepenArgs::from_opts(opts, &local_shallow);
let (pack, su) = negotiate_pack_v2_http(
client,
local_git_dir,
&post_url,
&content_type,
&accept,
git_protocol,
&server_caps,
&local_odb,
&need,
&deepen,
progress,
)?;
shallow_update = su;
if !pack.is_empty() {
if pack.len() < 12 || &pack[0..4] != b"PACK" {
return Err(Error::Message(
"did not receive a valid pack from v2 HTTP fetch".to_owned(),
));
}
let mut cursor = 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 opts.tags == TagMode::Following {
retain_following_tags(&local_odb, &mut matched, &wants);
}
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,
});
}
crate::net_trace::net_trace!("http_fetch (v2): done — {} ref update(s)", updates.len());
Ok(FetchOutcome {
updates,
default_branch,
new_shallow: shallow_update.shallow,
new_unshallow: shallow_update.unshallow,
})
}
#[allow(clippy::too_many_arguments)]
fn negotiate_pack_v2_http(
client: &dyn HttpClient,
local_git_dir: &Path,
post_url: &str,
content_type: &str,
accept: &str,
git_protocol: &str,
server_caps: &[String],
local_odb: &crate::odb::Odb,
wants: &[ObjectId],
deepen: &crate::fetch::V2DeepenArgs,
progress: &mut dyn Progress,
) -> Result<(Vec<u8>, crate::fetch::ShallowUpdate)> {
if wants.is_empty() {
return Ok((Vec::new(), crate::fetch::ShallowUpdate::default()));
}
let object_format = crate::fetch::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 {
crate::fetch::v2_local_haves(local_git_dir, wants)?
};
let mut pack = Vec::new();
let mut shallow_update = crate::fetch::ShallowUpdate::default();
if haves.is_empty() {
let mut req = Vec::new();
crate::fetch::write_v2_fetch_request(
&mut req,
&object_format,
&cap_echo,
wants,
&[],
sideband_all,
deepen,
true,
)?;
let resp = client.post(post_url, content_type, accept, &req, Some(git_protocol))?;
let mut cur = Cursor::new(resp);
crate::fetch::read_v2_fetch_pack_response(&mut cur, &mut pack, &mut shallow_update, progress)?;
return Ok((pack, shallow_update));
}
const INITIAL_FLUSH: usize = 16;
let mut flush_at: usize = INITIAL_FLUSH.min(haves.len());
loop {
if flush_at < haves.len() {
let mut req = Vec::new();
crate::fetch::write_v2_fetch_request(
&mut req,
&object_format,
&cap_echo,
wants,
&haves[..flush_at],
sideband_all,
deepen,
false,
)?;
let resp = client.post(post_url, content_type, accept, &req, Some(git_protocol))?;
let mut cur = Cursor::new(resp);
let ack = crate::fetch::read_v2_acknowledgments(&mut cur)?;
if let Some(round) = ack {
if round.ready {
crate::fetch::read_v2_fetch_pack_response(
&mut cur,
&mut pack,
&mut shallow_update,
progress,
)?;
return Ok((pack, shallow_update));
}
} else {
crate::fetch::read_v2_fetch_pack_response(
&mut cur,
&mut pack,
&mut shallow_update,
progress,
)?;
return Ok((pack, shallow_update));
}
flush_at = next_flush(flush_at).min(haves.len());
continue;
}
let mut req = Vec::new();
crate::fetch::write_v2_fetch_request(
&mut req,
&object_format,
&cap_echo,
wants,
&haves,
sideband_all,
deepen,
true,
)?;
let resp = client.post(post_url, content_type, accept, &req, Some(git_protocol))?;
let mut cur = Cursor::new(resp);
crate::fetch::read_v2_fetch_pack_response(&mut cur, &mut pack, &mut shallow_update, progress)?;
return Ok((pack, shallow_update));
}
}
fn retain_following_tags(
odb: &crate::odb::Odb,
matched: &mut Vec<crate::transfer::MatchedRef>,
wants: &HashSet<ObjectId>,
) {
let roots: Vec<ObjectId> = matched.iter().filter(|m| !m.is_tag).map(|m| m.oid).collect();
let closure = reachable_closure(odb, &roots);
matched.retain(|m| {
if !m.is_tag {
return true;
}
let peeled = peel_tag_target(odb, m.oid);
let have = odb.exists(&m.oid);
have && (closure.contains(&m.oid) || closure.contains(&peeled) || wants.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
}
pub fn discovery_advertisement(conn: &SmartHttpConnection) -> Advertisement {
Advertisement {
refs: conn.adv_refs.clone(),
capabilities: conn.caps.clone(),
head_symref: conn.head_symref.clone(),
protocol_version: conn.protocol_version,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strips_smart_service_preamble() {
let mut body = Vec::new();
pkt_line::write_line_to_vec(&mut body, "# service=git-upload-pack\n").unwrap();
body.extend_from_slice(b"0000");
let oid = "1".repeat(40);
let line = format!("{oid} refs/heads/main\0multi_ack_detailed side-band-64k");
pkt_line::write_line_to_vec(&mut body, &line).unwrap();
body.extend_from_slice(b"0000");
let stripped = strip_service_advertisement(&body).unwrap();
let disc = parse_advertisement(stripped).unwrap();
assert_eq!(disc.protocol_version, 0);
assert_eq!(disc.refs.len(), 1);
assert_eq!(disc.refs[0].name, "refs/heads/main");
assert!(disc.caps.contains("side-band-64k"));
}
#[test]
fn parses_symref_and_caps() {
let mut body = Vec::new();
let main = "2".repeat(40);
let head = format!(
"{main} HEAD\0multi_ack_detailed symref=HEAD:refs/heads/main object-format=sha1"
);
pkt_line::write_line_to_vec(&mut body, &head).unwrap();
let r = format!("{main} refs/heads/main");
pkt_line::write_line_to_vec(&mut body, &r).unwrap();
body.extend_from_slice(b"0000");
let disc = parse_advertisement(&body).unwrap();
assert_eq!(disc.head_symref.as_deref(), Some("refs/heads/main"));
assert_eq!(disc.object_format, "sha1");
assert!(disc.refs.iter().any(|r| r.name == "HEAD"));
assert!(disc.refs.iter().any(|r| r.name == "refs/heads/main"));
}
#[test]
fn detects_v2_preamble() {
let mut body = Vec::new();
pkt_line::write_line_to_vec(&mut body, "version 2").unwrap();
pkt_line::write_line_to_vec(&mut body, "ls-refs").unwrap();
pkt_line::write_line_to_vec(&mut body, "object-format=sha256").unwrap();
body.extend_from_slice(b"0000");
let disc = parse_advertisement(&body).unwrap();
assert_eq!(disc.protocol_version, 2);
assert_eq!(disc.object_format, "sha256");
}
#[test]
fn url_helpers() {
assert_eq!(
info_refs_url("http://h/r.git"),
"http://h/r.git/info/refs?service=git-upload-pack"
);
assert_eq!(
info_refs_url("http://h/r.git/"),
"http://h/r.git/info/refs?service=git-upload-pack"
);
assert_eq!(upload_pack_url("http://h/r.git/"), "http://h/r.git/git-upload-pack");
}
}