use crate::error::{Error, Result};
use crate::objects::{parse_commit, ObjectId, ObjectKind};
use crate::odb::Odb;
use crate::pkt_line;
use std::collections::HashSet;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
pub const INFINITE_DEPTH: u32 = 0x7fff_ffff;
#[must_use]
pub fn shallow_file_path(git_dir: &Path) -> PathBuf {
crate::repo::common_git_dir_for_config(git_dir).join("shallow")
}
#[must_use]
pub fn load_shallow_boundaries(git_dir: &Path) -> HashSet<ObjectId> {
let shallow_path = shallow_file_path(git_dir);
let mut set = HashSet::new();
let Ok(contents) = fs::read_to_string(&shallow_path) else {
return set;
};
for line in contents.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if let Ok(oid) = line.parse::<ObjectId>() {
set.insert(oid);
}
}
set
}
#[must_use]
pub fn dedupe_shallow_list(ids: Vec<ObjectId>) -> Vec<ObjectId> {
let mut seen = HashSet::new();
ids.into_iter().filter(|oid| seen.insert(*oid)).collect()
}
pub fn add_shallow_boundaries(
git_dir: &Path,
new_boundaries: &HashSet<ObjectId>,
) -> std::io::Result<()> {
if new_boundaries.is_empty() {
return Ok(());
}
let mut all = load_shallow_boundaries(git_dir);
all.extend(new_boundaries.iter().copied());
let mut hexes: Vec<String> = all.iter().map(ObjectId::to_hex).collect();
hexes.sort();
let mut body = hexes.join("\n");
body.push('\n');
fs::write(shallow_file_path(git_dir), body)
}
#[must_use]
pub fn new_shallow_roots_for_push(
source_odb: &Odb,
tip: ObjectId,
boundaries: &HashSet<ObjectId>,
have_tips: &[ObjectId],
) -> HashSet<ObjectId> {
let mut new_roots = HashSet::new();
if boundaries.is_empty() {
return new_roots;
}
let mut have: HashSet<ObjectId> = HashSet::new();
let mut have_stack: Vec<ObjectId> = have_tips.to_vec();
while let Some(oid) = have_stack.pop() {
if !have.insert(oid) {
continue;
}
if boundaries.contains(&oid) {
continue;
}
if let Some(parents) = commit_parents(source_odb, &oid) {
have_stack.extend(parents);
}
}
let mut seen: HashSet<ObjectId> = HashSet::new();
let mut stack = vec![tip];
while let Some(oid) = stack.pop() {
if have.contains(&oid) {
continue;
}
if !seen.insert(oid) {
continue;
}
if boundaries.contains(&oid) {
new_roots.insert(oid);
continue;
}
if let Some(parents) = commit_parents(source_odb, &oid) {
stack.extend(parents);
}
}
new_roots
}
pub fn load_shallow_oids(git_dir: &Path) -> Result<Vec<ObjectId>> {
let shallow_path = shallow_file_path(git_dir);
if !shallow_path.exists() {
return Ok(Vec::new());
}
let contents = fs::read_to_string(&shallow_path).map_err(Error::Io)?;
let mut out = Vec::new();
for line in contents.lines().map(str::trim).filter(|l| !l.is_empty()) {
if let Ok(oid) = ObjectId::from_hex(line) {
out.push(oid);
}
}
Ok(out)
}
pub fn apply_shallow_updates(
git_dir: &Path,
shallow: &[ObjectId],
unshallow: &[ObjectId],
) -> Result<()> {
if shallow.is_empty() && unshallow.is_empty() {
return Ok(());
}
let mut boundaries: HashSet<ObjectId> = load_shallow_oids(git_dir)?.into_iter().collect();
for oid in shallow {
boundaries.insert(*oid);
}
for oid in unshallow {
boundaries.remove(oid);
}
let shallow_path = shallow_file_path(git_dir);
if boundaries.is_empty() {
let _ = fs::remove_file(&shallow_path);
return Ok(());
}
let mut lines: Vec<String> = boundaries.iter().map(ObjectId::to_hex).collect();
lines.sort();
let mut contents = lines.join("\n");
contents.push('\n');
fs::write(&shallow_path, contents).map_err(Error::Io)
}
pub(crate) fn v2_primary_line_from_pkt(
payload: String,
sideband_all: bool,
) -> Result<Option<String>> {
if !sideband_all {
return Ok(Some(payload.trim_end().to_owned()));
}
let bytes = payload.as_bytes();
if bytes.is_empty() {
return Ok(None);
}
match bytes[0] {
1 => Ok(Some(
String::from_utf8_lossy(&bytes[1..]).trim_end().to_owned(),
)),
2 => Ok(None),
3 => Err(Error::Message(format!(
"remote error: {}",
String::from_utf8_lossy(&bytes[1..]).trim_end()
))),
_ => Ok(Some(payload.trim_end().to_owned())),
}
}
pub(crate) fn read_v2_fetch_primary_line(
r: &mut dyn Read,
sideband_all: bool,
) -> Result<Option<String>> {
loop {
match pkt_line::read_packet(&mut &mut *r)? {
None | Some(pkt_line::Packet::Flush) | Some(pkt_line::Packet::Delim) => {
return Ok(None);
}
Some(pkt_line::Packet::ResponseEnd) => return Ok(None),
Some(pkt_line::Packet::Data(payload)) => {
if let Some(line) = v2_primary_line_from_pkt(payload, sideband_all)? {
return Ok(Some(line));
}
}
}
}
}
pub fn read_shallow_info_section(r: &mut dyn Read) -> Result<(Vec<ObjectId>, Vec<ObjectId>)> {
read_shallow_info_section_sideband(r, false)
}
pub fn read_shallow_info_section_sideband(
r: &mut dyn Read,
sideband_all: bool,
) -> Result<(Vec<ObjectId>, Vec<ObjectId>)> {
let mut shallow = Vec::new();
let mut unshallow = Vec::new();
while let Some(line) = read_v2_fetch_primary_line(r, sideband_all)? {
if line.is_empty() {
continue;
}
if let Some(rest) = line.strip_prefix("shallow ") {
let oid = ObjectId::from_hex(rest.trim())
.map_err(|_| Error::Message(format!("parse shallow oid {}", rest.trim())))?;
shallow.push(oid);
} else if let Some(rest) = line.strip_prefix("unshallow ") {
let oid = ObjectId::from_hex(rest.trim())
.map_err(|_| Error::Message(format!("parse unshallow oid {}", rest.trim())))?;
unshallow.push(oid);
}
}
Ok((shallow, unshallow))
}
#[must_use]
pub fn deepen_since_wire_value(since: &str) -> String {
let since = since.trim();
if since.parse::<u64>().is_ok() {
return since.to_owned();
}
crate::git_date::approx::approxidate_careful(since, None).to_string()
}
fn commit_parents(odb: &Odb, oid: &ObjectId) -> Option<Vec<ObjectId>> {
let obj = odb.read(oid).ok()?;
if obj.kind != ObjectKind::Commit {
return None;
}
let commit = parse_commit(&obj.data).ok()?;
Some(commit.parents)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
const EMPTY_TREE: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
fn write_commit(odb: &Odb, parents: &[ObjectId], msg: &str) -> ObjectId {
let mut body = format!("tree {EMPTY_TREE}\n");
for p in parents {
body.push_str(&format!("parent {}\n", p.to_hex()));
}
body.push_str("author A <a@x> 1 +0000\ncommitter A <a@x> 1 +0000\n\n");
body.push_str(msg);
body.push('\n');
odb.write_loose_materialize(ObjectKind::Commit, body.as_bytes())
.expect("write commit")
}
#[test]
fn no_boundaries_means_no_shallow_roots() {
let dir = tempdir().unwrap();
let odb = Odb::new(&dir.path().join("objects"));
let root = write_commit(&odb, &[], "root");
let tip = write_commit(&odb, &[root], "tip");
let roots = new_shallow_roots_for_push(&odb, tip, &HashSet::new(), &[]);
assert!(roots.is_empty());
}
#[test]
fn boundary_not_on_receiver_is_a_new_root() {
let dir = tempdir().unwrap();
let odb = Odb::new(&dir.path().join("objects"));
let a = write_commit(&odb, &[], "a");
let b = write_commit(&odb, &[a], "b");
let c = write_commit(&odb, &[b], "c");
let mut boundaries = HashSet::new();
boundaries.insert(a);
let roots = new_shallow_roots_for_push(&odb, c, &boundaries, &[]);
assert_eq!(roots, boundaries);
}
#[test]
fn boundary_already_on_receiver_is_not_a_new_root() {
let dir = tempdir().unwrap();
let odb = Odb::new(&dir.path().join("objects"));
let a = write_commit(&odb, &[], "a");
let b = write_commit(&odb, &[a], "b");
let c = write_commit(&odb, &[b], "c");
let mut boundaries = HashSet::new();
boundaries.insert(a);
let roots = new_shallow_roots_for_push(&odb, c, &boundaries, &[b]);
assert!(roots.is_empty());
}
#[test]
fn add_shallow_boundaries_unions_existing() {
let dir = tempdir().unwrap();
let git_dir = dir.path();
let a = ObjectId::from_hex("1111111111111111111111111111111111111111").unwrap();
let b = ObjectId::from_hex("2222222222222222222222222222222222222222").unwrap();
let mut first = HashSet::new();
first.insert(a);
add_shallow_boundaries(git_dir, &first).unwrap();
let mut second = HashSet::new();
second.insert(b);
add_shallow_boundaries(git_dir, &second).unwrap();
let loaded = load_shallow_boundaries(git_dir);
assert!(loaded.contains(&a));
assert!(loaded.contains(&b));
assert_eq!(loaded.len(), 2);
}
fn oid(hex_byte: u8) -> ObjectId {
let s = format!("{hex_byte:02x}").repeat(20);
ObjectId::from_hex(&s).unwrap()
}
#[test]
fn linked_worktree_admin_dir_uses_common_shallow_file() {
let dir = tempdir().unwrap();
let common = dir.path().join("repo.git");
let admin = common.join("worktrees").join("linked");
std::fs::create_dir_all(&admin).unwrap();
std::fs::write(admin.join("commondir"), "../..\n").unwrap();
let common = common.canonicalize().unwrap();
assert_eq!(shallow_file_path(&admin), common.join("shallow"));
assert_eq!(shallow_file_path(&common), common.join("shallow"));
let a = oid(0x11);
let b = oid(0x22);
std::fs::write(common.join("shallow"), format!("{}\n", a.to_hex())).unwrap();
assert_eq!(load_shallow_boundaries(&admin), HashSet::from([a]));
assert_eq!(load_shallow_oids(&admin).unwrap(), vec![a]);
apply_shallow_updates(&admin, &[b], &[a]).unwrap();
assert!(!admin.join("shallow").exists());
assert_eq!(load_shallow_boundaries(&common), HashSet::from([b]));
apply_shallow_updates(&admin, &[], &[b]).unwrap();
assert!(!common.join("shallow").exists());
}
#[test]
fn load_shallow_oids_reads_file_in_order() {
let dir = tempdir().unwrap();
let git_dir = dir.path();
assert!(load_shallow_oids(git_dir).unwrap().is_empty());
let a = oid(0x11);
let b = oid(0x22);
std::fs::write(
git_dir.join("shallow"),
format!("{}\n{}\n", a.to_hex(), b.to_hex()),
)
.unwrap();
assert_eq!(load_shallow_oids(git_dir).unwrap(), vec![a, b]);
}
#[test]
fn read_shallow_info_section_parses_shallow_and_unshallow() {
let a = oid(0xaa);
let b = oid(0xbb);
let mut bytes = Vec::new();
pkt_line::write_line_to_vec(&mut bytes, &format!("shallow {}", a.to_hex())).unwrap();
pkt_line::write_line_to_vec(&mut bytes, &format!("unshallow {}", b.to_hex())).unwrap();
pkt_line::write_flush(&mut bytes).unwrap();
bytes.extend_from_slice(b"PACK");
let mut cur = std::io::Cursor::new(bytes);
let (shallow, unshallow) = read_shallow_info_section(&mut cur).unwrap();
assert_eq!(shallow, vec![a]);
assert_eq!(unshallow, vec![b]);
let mut rest = Vec::new();
std::io::Read::read_to_end(&mut cur, &mut rest).unwrap();
assert_eq!(&rest, b"PACK");
}
#[test]
fn v2_primary_line_from_pkt_strips_sideband_all_header() {
let payload = format!("\u{1}packfile\n");
let line = v2_primary_line_from_pkt(payload, true).unwrap().unwrap();
assert_eq!(line, "packfile");
}
#[test]
fn read_shallow_info_section_stops_at_delim_for_v2() {
let a = oid(0xcc);
let mut bytes = Vec::new();
pkt_line::write_line_to_vec(&mut bytes, &format!("shallow {}", a.to_hex())).unwrap();
pkt_line::write_delim(&mut bytes).unwrap();
pkt_line::write_line_to_vec(&mut bytes, "packfile").unwrap();
let mut cur = std::io::Cursor::new(bytes);
let (shallow, unshallow) = read_shallow_info_section(&mut cur).unwrap();
assert_eq!(shallow, vec![a]);
assert!(unshallow.is_empty());
match pkt_line::read_packet(&mut cur).unwrap() {
Some(pkt_line::Packet::Data(s)) => assert_eq!(s.trim_end(), "packfile"),
other => panic!("expected packfile header, got {other:?}"),
}
}
#[test]
fn apply_shallow_updates_adds_removes_and_deletes() {
let dir = tempdir().unwrap();
let git_dir = dir.path();
let a = oid(0x01);
let b = oid(0x02);
apply_shallow_updates(git_dir, &[a, b], &[]).unwrap();
let loaded: HashSet<ObjectId> = load_shallow_oids(git_dir).unwrap().into_iter().collect();
assert_eq!(loaded, HashSet::from([a, b]));
apply_shallow_updates(git_dir, &[], &[a]).unwrap();
assert_eq!(load_shallow_oids(git_dir).unwrap(), vec![b]);
apply_shallow_updates(git_dir, &[], &[b]).unwrap();
assert!(!git_dir.join("shallow").exists());
assert!(load_shallow_oids(git_dir).unwrap().is_empty());
}
#[test]
fn deepen_since_passes_through_bare_timestamp() {
assert_eq!(deepen_since_wire_value("200000000"), "200000000");
assert_eq!(deepen_since_wire_value(" 1234567890 "), "1234567890");
let v = deepen_since_wire_value("2005-04-07 22:13:13 +0200");
assert!(
v.parse::<u64>().is_ok(),
"expected a bare timestamp, got {v:?}"
);
}
}