use std::collections::{HashMap, HashSet, VecDeque};
use std::io::Write;
use std::path::Path;
use flate2::write::ZlibEncoder;
use flate2::Compression;
use sha1::{Digest as _, Sha1};
use sha2::Sha256;
use crate::delta_encode::{encode_lcp_delta, encode_prefix_extension_delta};
use crate::error::{Error, Result};
use crate::objects::{
parse_commit, parse_tag, parse_tree, HashAlgo, Object, ObjectId, ObjectKind,
};
use crate::odb::Odb;
use crate::push_report::{PushRefResult, PushRefStatus};
use crate::refspec::{parse_fetch_refspec, RefspecItem};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UpdateMode {
New,
FastForward,
Forced,
UpToDate,
NoChangeNeeded,
NonFastForwardRejected,
TagUpdateRejected,
SourceObjectNotFound,
Unborn,
DeletedMissing,
}
#[derive(Clone, Debug)]
pub struct RefUpdate {
pub remote_ref: String,
pub local_ref: Option<String>,
pub old_oid: Option<ObjectId>,
pub new_oid: Option<ObjectId>,
pub mode: UpdateMode,
pub note: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum TagMode {
None,
#[default]
Following,
All,
}
#[derive(Clone, Debug)]
pub struct FetchOptions {
pub refspecs: Vec<String>,
pub negative_refspecs: Vec<String>,
pub tags: TagMode,
pub prune: bool,
pub dry_run: bool,
pub depth: Option<u32>,
pub deepen_since: Option<String>,
pub deepen_not: Vec<String>,
pub unshallow: bool,
}
impl Default for FetchOptions {
fn default() -> Self {
Self {
refspecs: Vec::new(),
negative_refspecs: Vec::new(),
tags: TagMode::default(),
prune: false,
dry_run: false,
depth: None,
deepen_since: None,
deepen_not: Vec::new(),
unshallow: false,
}
}
}
impl FetchOptions {
#[must_use]
pub fn has_deepen_request(&self) -> bool {
self.depth.is_some()
|| self
.deepen_since
.as_deref()
.is_some_and(|v| !v.trim().is_empty())
|| self.deepen_not.iter().any(|v| !v.trim().is_empty())
|| self.unshallow
}
}
#[derive(Clone, Debug, Default)]
pub struct FetchOutcome {
pub updates: Vec<RefUpdate>,
pub default_branch: Option<String>,
pub new_shallow: Vec<ObjectId>,
pub new_unshallow: Vec<ObjectId>,
}
#[derive(Clone, Debug)]
pub struct PushRefSpec {
pub src: Option<ObjectId>,
pub dst: String,
pub force: bool,
pub delete: bool,
pub expected_old: Option<ObjectId>,
pub expect_absent: bool,
}
#[derive(Clone, Debug, Default)]
pub struct PushOptions {
pub atomic: bool,
pub dry_run: bool,
pub push_options: Vec<String>,
}
#[derive(Clone, Debug, Default)]
pub struct PushOutcome {
pub results: Vec<PushRefResult>,
}
#[derive(Clone, Copy, Debug)]
pub struct PackBuildOptions {
pub thin: bool,
pub delta: bool,
pub window: usize,
pub max_depth: usize,
pub use_ofs_delta: bool,
pub respect_islands: bool,
pub reuse_deltas: bool,
}
impl Default for PackBuildOptions {
fn default() -> Self {
Self {
thin: false,
delta: false,
window: 10,
max_depth: 50,
use_ofs_delta: true,
respect_islands: false,
reuse_deltas: false,
}
}
}
pub fn build_pack(
odb: &Odb,
wants: &[ObjectId],
haves: &[ObjectId],
opts: &PackBuildOptions,
) -> Result<Vec<u8>> {
let have_closure = reachable_closure(odb, haves, &HashSet::new(), true)?;
let send = collect_reachable_excluding(odb, wants, &have_closure, false)?;
if !opts.delta {
return serialize_pack(odb, &send);
}
let plan = plan_deltas(odb, &send, &have_closure, opts)?;
serialize_pack_with_deltas(odb, &plan, opts)
}
fn reachable_closure(
odb: &Odb,
roots: &[ObjectId],
stop: &HashSet<ObjectId>,
skip_missing: bool,
) -> Result<HashSet<ObjectId>> {
let mut seen = HashSet::new();
let order = collect_reachable_excluding(odb, roots, stop, skip_missing)?;
for oid in order {
seen.insert(oid);
}
Ok(seen)
}
fn collect_reachable_excluding(
odb: &Odb,
roots: &[ObjectId],
exclude: &HashSet<ObjectId>,
skip_missing: bool,
) -> Result<Vec<ObjectId>> {
let mut visited: HashSet<ObjectId> = HashSet::new();
let mut ordered: Vec<ObjectId> = Vec::new();
let mut queue: VecDeque<ObjectId> = VecDeque::new();
let enqueue = |oid: ObjectId,
queue: &mut VecDeque<ObjectId>,
visited: &mut HashSet<ObjectId>,
ordered: &mut Vec<ObjectId>|
-> bool {
if exclude.contains(&oid) {
return false;
}
if visited.insert(oid) {
ordered.push(oid);
queue.push_back(oid);
true
} else {
false
}
};
for &root in roots {
enqueue(root, &mut queue, &mut visited, &mut ordered);
}
while let Some(oid) = queue.pop_front() {
let obj = match odb.read(&oid) {
Ok(o) => o,
Err(_) if skip_missing => continue,
Err(e) => return Err(e),
};
match obj.kind {
ObjectKind::Commit => {
let commit = parse_commit(&obj.data)?;
for parent in commit.parents {
enqueue(parent, &mut queue, &mut visited, &mut ordered);
}
enqueue(commit.tree, &mut queue, &mut visited, &mut ordered);
}
ObjectKind::Tree => {
for entry in parse_tree(&obj.data)? {
if entry.mode == 0o160000 {
continue;
}
enqueue(entry.oid, &mut queue, &mut visited, &mut ordered);
}
}
ObjectKind::Tag => {
let tag = parse_tag(&obj.data)?;
enqueue(tag.object, &mut queue, &mut visited, &mut ordered);
}
ObjectKind::Blob => {}
}
}
Ok(ordered)
}
fn pack_type_code(kind: ObjectKind) -> u8 {
match kind {
ObjectKind::Commit => 1,
ObjectKind::Tree => 2,
ObjectKind::Blob => 3,
ObjectKind::Tag => 4,
}
}
fn encode_pack_object_header(buf: &mut Vec<u8>, type_code: u8, payload_len: usize) {
let mut size = payload_len;
let first = ((type_code & 0x7) << 4) | (size & 0x0f) as u8;
size >>= 4;
if size > 0 {
buf.push(first | 0x80);
while size > 0 {
let b = (size & 0x7f) as u8;
size >>= 7;
buf.push(if size > 0 { b | 0x80 } else { b });
}
} else {
buf.push(first);
}
}
fn serialize_pack(odb: &Odb, oids: &[ObjectId]) -> Result<Vec<u8>> {
let mut buf = Vec::new();
buf.extend_from_slice(b"PACK");
buf.extend_from_slice(&2u32.to_be_bytes());
let count = u32::try_from(oids.len())
.map_err(|_| Error::CorruptObject("pack object count exceeds u32".to_owned()))?;
buf.extend_from_slice(&count.to_be_bytes());
for oid in oids {
let obj = odb.read(oid)?;
encode_pack_object_header(&mut buf, pack_type_code(obj.kind), obj.data.len());
let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
enc.write_all(&obj.data).map_err(Error::Io)?;
let compressed = enc.finish().map_err(Error::Io)?;
buf.extend_from_slice(&compressed);
}
append_pack_trailer(&mut buf, odb.hash_algo());
Ok(buf)
}
fn append_pack_trailer(buf: &mut Vec<u8>, algo: HashAlgo) {
match algo {
HashAlgo::Sha1 => {
let mut hasher = Sha1::new();
hasher.update(&*buf);
buf.extend_from_slice(&hasher.finalize());
}
HashAlgo::Sha256 => {
let mut hasher = Sha256::new();
hasher.update(&*buf);
buf.extend_from_slice(&hasher.finalize());
}
}
}
struct PlannedEntry {
oid: ObjectId,
kind: ObjectKind,
data: Vec<u8>,
base: Option<ObjectId>,
reused_delta: Option<Vec<u8>>,
}
struct DeltaPlan {
entries: Vec<PlannedEntry>,
#[allow(dead_code)]
external_bases: HashSet<ObjectId>,
}
fn common_prefix_len(a: &[u8], b: &[u8]) -> usize {
a.iter()
.zip(b.iter())
.take_while(|(left, right)| left == right)
.count()
}
fn apply_delta_depth_limit(map: &mut HashMap<ObjectId, ObjectId>, max_depth: usize) {
let keys: Vec<ObjectId> = map.keys().copied().collect();
let value_set: HashSet<ObjectId> = map.values().copied().collect();
let tips: Vec<ObjectId> = keys
.into_iter()
.filter(|k| !value_set.contains(k))
.collect();
let modulus = max_depth.saturating_add(1);
let mut snip: HashSet<ObjectId> = HashSet::new();
for tip in tips {
let mut chain: Vec<ObjectId> = Vec::new();
let mut cur = tip;
let mut seen = HashSet::new();
while seen.insert(cur) {
chain.push(cur);
let Some(&b) = map.get(&cur) else {
break;
};
cur = b;
}
let n = chain.len();
if n < 2 {
continue;
}
let mut total_depth = (n - 1) as u32;
for &oid in &chain {
let assigned = (total_depth as usize) % modulus;
total_depth = total_depth.saturating_sub(1);
if assigned == 0 {
snip.insert(oid);
}
}
}
for oid in snip {
map.remove(&oid);
}
}
fn plan_deltas(
odb: &Odb,
send: &[ObjectId],
have_closure: &HashSet<ObjectId>,
opts: &PackBuildOptions,
) -> Result<DeltaPlan> {
let mut objects: HashMap<ObjectId, Object> = HashMap::new();
for &oid in send {
objects.insert(oid, odb.read(&oid)?);
}
let in_pack: HashSet<ObjectId> = send.iter().copied().collect();
let islands = load_islands_for_pack(odb, &in_pack, opts);
let mut delta_to_base: HashMap<ObjectId, ObjectId> = HashMap::new();
let mut reused: HashMap<ObjectId, Vec<u8>> = HashMap::new();
let mut external_bases: HashSet<ObjectId> = HashSet::new();
if opts.window > 0 && opts.max_depth > 0 {
if opts.reuse_deltas && odb.hash_algo() == HashAlgo::Sha1 {
let objects_dir = odb.objects_dir();
for &t in send {
if objects[&t].kind != ObjectKind::Blob || objects[&t].data.is_empty() {
continue;
}
if let Ok(Some((base, zdelta))) =
crate::pack::packed_ref_delta_reuse_slice(objects_dir, &t, &in_pack)
{
if base != t
&& in_pack.contains(&base)
&& islands.in_same_island(&t, &base)
{
delta_to_base.insert(t, base);
reused.insert(t, zdelta);
}
}
}
}
let mut blobs: Vec<ObjectId> = send
.iter()
.copied()
.filter(|oid| objects[oid].kind == ObjectKind::Blob && !objects[oid].data.is_empty())
.collect();
blobs.sort_by_key(|oid| objects[oid].data.len());
let mut external_blob_data: HashMap<ObjectId, Vec<u8>> = HashMap::new();
if opts.thin {
for &oid in have_closure {
if in_pack.contains(&oid) {
continue;
}
if let Ok(obj) = odb.read(&oid) {
if obj.kind == ObjectKind::Blob && !obj.data.is_empty() {
external_blob_data.insert(oid, obj.data);
}
}
}
}
for (i, &t) in blobs.iter().enumerate() {
if delta_to_base.contains_key(&t) {
continue;
}
let t_data = &objects[&t].data;
let mut best: Option<(ObjectId, usize, usize, bool)> = None;
let mut considered = 0usize;
for &b in blobs.iter().skip(i + 1) {
if considered >= opts.window {
break;
}
considered += 1;
if !islands.in_same_island(&t, &b) {
continue;
}
let b_data = &objects[&b].data;
if b_data.len() <= t_data.len() {
continue;
}
let common = if b_data.starts_with(t_data) {
t_data.len()
} else {
common_prefix_len(t_data, b_data)
};
if common > 64 && common.saturating_mul(2) >= t_data.len() {
let better = best.is_none_or(|(prev_b, bc, bl, _)| {
if islands.is_active() {
let cmp = islands.delta_cmp(&b, &prev_b);
if cmp < 0 {
return true;
}
if cmp > 0 {
return false;
}
}
common > bc || (common == bc && b_data.len() < bl)
});
if better {
best = Some((b, common, b_data.len(), false));
}
}
}
if opts.thin {
for (&b, b_data) in &external_blob_data {
if b == t {
continue;
}
if !islands.in_same_island(&t, &b) {
continue;
}
let common = common_prefix_len(t_data, b_data);
if common > 64 && common.saturating_mul(2) >= t_data.len() {
let better = best.is_none_or(|(_, bc, bl, ext)| {
common > bc || (common == bc && ext && b_data.len() < bl)
});
if better {
best = Some((b, common, b_data.len(), true));
}
}
}
}
if let Some((base, _, _, external)) = best {
delta_to_base.insert(t, base);
if external {
external_bases.insert(base);
if let Some(d) = external_blob_data.get(&base) {
objects
.entry(base)
.or_insert_with(|| Object::new(ObjectKind::Blob, d.clone()));
}
}
}
}
apply_delta_depth_limit(&mut delta_to_base, opts.max_depth);
reused.retain(|t, _| delta_to_base.contains_key(t));
external_bases.retain(|b| delta_to_base.values().any(|v| v == b));
}
let mut entries: Vec<PlannedEntry> = Vec::with_capacity(send.len());
for &oid in send {
let obj = &objects[&oid];
entries.push(PlannedEntry {
oid,
kind: obj.kind,
data: obj.data.clone(),
base: delta_to_base.get(&oid).copied(),
reused_delta: reused.get(&oid).cloned(),
});
}
Ok(DeltaPlan {
entries,
external_bases,
})
}
fn load_islands_for_pack(
odb: &Odb,
in_pack: &HashSet<ObjectId>,
opts: &PackBuildOptions,
) -> crate::delta_islands::DeltaIslands {
if !opts.respect_islands {
return crate::delta_islands::DeltaIslands::default();
}
let Some(git_dir) = odb.config_git_dir() else {
return crate::delta_islands::DeltaIslands::default();
};
let Ok(repo) = crate::repo::Repository::open(git_dir, None) else {
return crate::delta_islands::DeltaIslands::default();
};
let cfg = crate::config::ConfigSet::load(Some(git_dir), true).unwrap_or_default();
crate::delta_islands::load_delta_islands(&repo, &cfg, in_pack)
}
fn serialize_pack_with_deltas(
odb: &Odb,
plan: &DeltaPlan,
opts: &PackBuildOptions,
) -> Result<Vec<u8>> {
let algo = odb.hash_algo();
let mut buf = Vec::new();
buf.extend_from_slice(b"PACK");
buf.extend_from_slice(&2u32.to_be_bytes());
let count = u32::try_from(plan.entries.len())
.map_err(|_| Error::CorruptObject("pack object count exceeds u32".to_owned()))?;
buf.extend_from_slice(&count.to_be_bytes());
let payloads: HashMap<ObjectId, &[u8]> =
plan.entries.iter().map(|e| (e.oid, e.data.as_slice())).collect();
let mut oid_to_offset: HashMap<ObjectId, u64> = HashMap::new();
for entry in &plan.entries {
let start = buf.len() as u64;
match entry.base {
None => {
encode_pack_object_header(&mut buf, pack_type_code(entry.kind), entry.data.len());
write_zlib(&mut buf, &entry.data)?;
oid_to_offset.insert(entry.oid, start);
}
Some(base_oid) => {
let delta = if let Some(reused) = &entry.reused_delta {
reused.clone()
} else {
let base_data: Vec<u8> = if let Some(d) = payloads.get(&base_oid) {
d.to_vec()
} else {
odb.read(&base_oid)?.data
};
if entry.data.starts_with(&base_data) && entry.data.len() > base_data.len() {
encode_prefix_extension_delta(&base_data, &entry.data)?
} else {
encode_lcp_delta(&base_data, &entry.data)?
}
};
let in_pack_offset = oid_to_offset.get(&base_oid).copied();
if opts.use_ofs_delta && in_pack_offset.is_some() {
let base_off = in_pack_offset.expect("checked is_some");
let dist = start.checked_sub(base_off).ok_or_else(|| {
Error::CorruptObject("ofs-delta distance underflow".to_owned())
})?;
encode_pack_object_header(&mut buf, 6, delta.len());
encode_ofs_delta_distance(&mut buf, dist);
} else {
encode_pack_object_header(&mut buf, 7, delta.len());
if base_oid.as_bytes().len() != algo.len() {
return Err(Error::CorruptObject(
"ref-delta base oid width mismatch".to_owned(),
));
}
buf.extend_from_slice(base_oid.as_bytes());
}
write_zlib(&mut buf, &delta)?;
oid_to_offset.insert(entry.oid, start);
}
}
}
append_pack_trailer(&mut buf, algo);
Ok(buf)
}
fn write_zlib(buf: &mut Vec<u8>, data: &[u8]) -> Result<()> {
let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
enc.write_all(data).map_err(Error::Io)?;
let compressed = enc.finish().map_err(Error::Io)?;
buf.extend_from_slice(&compressed);
Ok(())
}
fn encode_ofs_delta_distance(buf: &mut Vec<u8>, mut ofs: u64) {
let mut dheader = [0u8; 32];
let mut pos = dheader.len() - 1;
dheader[pos] = (ofs & 0x7f) as u8;
while {
ofs >>= 7;
ofs != 0
} {
pos -= 1;
ofs -= 1;
dheader[pos] = 0x80 | ((ofs & 0x7f) as u8);
}
buf.extend_from_slice(&dheader[pos..]);
}
pub fn fetch_local(
local_git_dir: &Path,
remote_git_dir: &Path,
opts: &FetchOptions,
) -> Result<FetchOutcome> {
if !remote_git_dir.join("objects").is_dir() {
return Err(Error::Message(format!(
"could not find repository at '{}'",
remote_git_dir.display()
)));
}
let local_odb = open_odb(local_git_dir);
let remote_odb = open_odb(remote_git_dir);
let remote_entries = crate::ls_remote::ls_remote(
remote_git_dir,
&remote_odb,
&crate::ls_remote::Options {
symref: true,
..Default::default()
},
)?;
let mut default_branch = None;
let mut remote_refs: Vec<(String, ObjectId)> = Vec::new();
for entry in &remote_entries {
if entry.name == "HEAD" {
default_branch = entry
.symref_target
.as_ref()
.map(|t| t.strip_prefix("refs/heads/").unwrap_or(t).to_owned());
continue;
}
if entry.name.ends_with("^{}") {
continue;
}
remote_refs.push((entry.name.clone(), entry.oid));
}
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<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 name.starts_with("refs/tags/") {
}
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(MatchedRef {
remote_ref: name.clone(),
local_ref,
oid: *oid,
force: refspecs_force(name, &positive),
is_tag: name.starts_with("refs/tags/"),
});
}
}
}
apply_tag_mode(
opts.tags,
&remote_refs,
&remote_odb,
&negatives,
&mut matched,
&mut matched_oids,
&mut seen_remote_ref,
)?;
let wants: Vec<ObjectId> = matched_oids
.iter()
.copied()
.filter(|oid| !local_odb.exists(oid))
.collect();
let mut haves: Vec<ObjectId> = Vec::new();
let mut have_seen: HashSet<ObjectId> = HashSet::new();
for m in &matched {
if let Some(local_ref) = &m.local_ref {
if let Ok(old) = crate::refs::resolve_ref(local_git_dir, local_ref) {
if have_seen.insert(old) {
haves.push(old);
}
}
}
}
if !wants.is_empty() && !opts.dry_run {
let pack = build_pack(&remote_odb, &wants, &haves, &PackBuildOptions::default())?;
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()
},
)?;
}
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: Vec::new(),
new_unshallow: Vec::new(),
})
}
pub fn push_local(
local_git_dir: &Path,
remote_git_dir: &Path,
refs: &[PushRefSpec],
opts: &PushOptions,
) -> Result<PushOutcome> {
let local_odb = open_odb(local_git_dir);
let remote_odb = open_odb(remote_git_dir);
let local_repo = crate::repo::Repository::open(local_git_dir, None).ok();
let remote_have_tips: Vec<ObjectId> = crate::refs::list_refs(remote_git_dir, "refs/")?
.into_iter()
.map(|(_, oid)| oid)
.collect();
let mut decisions: Vec<PushDecision> = Vec::with_capacity(refs.len());
for spec in refs {
decisions.push(decide_push(
spec,
&local_odb,
remote_git_dir,
local_repo.as_ref(),
)?);
}
let any_rejected = decisions.iter().any(|d| d.result.status.is_error());
if opts.atomic && any_rejected {
for d in &mut decisions {
if matches!(d.result.status, PushRefStatus::Ok) {
d.result.status = PushRefStatus::AtomicPushFailed;
d.apply = false;
}
}
return Ok(PushOutcome {
results: decisions.into_iter().map(|d| d.result).collect(),
});
}
for d in &mut decisions {
if !d.apply || opts.dry_run {
continue;
}
match &d.action {
PushAction::Update(src) => {
let pack = build_pack(
&local_odb,
&[*src],
&remote_have_tips,
&PackBuildOptions::default(),
)?;
let mut cursor = std::io::Cursor::new(pack);
crate::unpack_objects::unpack_objects(
&mut cursor,
&remote_odb,
&crate::unpack_objects::UnpackOptions {
quiet: true,
..Default::default()
},
)?;
crate::refs::write_ref(remote_git_dir, &d.result.remote_ref, src)?;
}
PushAction::Delete => {
crate::refs::delete_ref(remote_git_dir, &d.result.remote_ref)?;
}
PushAction::None => {}
}
}
Ok(PushOutcome {
results: decisions.into_iter().map(|d| d.result).collect(),
})
}
enum PushAction {
Update(ObjectId),
Delete,
None,
}
struct PushDecision {
result: PushRefResult,
action: PushAction,
apply: bool,
}
fn decide_push(
spec: &PushRefSpec,
local_odb: &Odb,
remote_git_dir: &Path,
local_repo: Option<&crate::repo::Repository>,
) -> Result<PushDecision> {
let remote_current = crate::refs::resolve_ref(remote_git_dir, &spec.dst).ok();
if !spec.delete {
if let Some(src) = spec.src {
if remote_current == Some(src) {
return Ok(PushDecision {
result: PushRefResult {
local_ref: None,
remote_ref: spec.dst.clone(),
old_oid: remote_current,
new_oid: Some(src),
forced: false,
deletion: false,
status: PushRefStatus::UpToDate,
message: None,
},
action: PushAction::None,
apply: false,
});
}
}
}
if spec.expect_absent && remote_current.is_some() {
return Ok(PushDecision {
result: PushRefResult {
local_ref: None,
remote_ref: spec.dst.clone(),
old_oid: remote_current,
new_oid: spec.src,
forced: false,
deletion: spec.delete,
status: PushRefStatus::RejectStale,
message: Some("stale info".to_owned()),
},
action: PushAction::None,
apply: false,
});
}
if let Some(expected) = spec.expected_old {
if remote_current != Some(expected) {
return Ok(PushDecision {
result: PushRefResult {
local_ref: None,
remote_ref: spec.dst.clone(),
old_oid: remote_current,
new_oid: spec.src,
forced: false,
deletion: spec.delete,
status: PushRefStatus::RejectStale,
message: Some("stale info".to_owned()),
},
action: PushAction::None,
apply: false,
});
}
}
if spec.delete {
let (status, action, apply) = match remote_current {
Some(_) => (PushRefStatus::Ok, PushAction::Delete, true),
None => (PushRefStatus::UpToDate, PushAction::None, false),
};
return Ok(PushDecision {
result: PushRefResult {
local_ref: None,
remote_ref: spec.dst.clone(),
old_oid: remote_current,
new_oid: None,
forced: false,
deletion: true,
status,
message: None,
},
action,
apply,
});
}
let Some(src) = spec.src else {
return Err(Error::Message(format!(
"push to '{}' has no source object and is not a deletion",
spec.dst
)));
};
if !local_odb.exists(&src) {
return Err(Error::Message(format!(
"source object {src} for '{}' is missing from the local object store",
spec.dst
)));
}
if remote_current == Some(src) {
return Ok(PushDecision {
result: PushRefResult {
local_ref: None,
remote_ref: spec.dst.clone(),
old_oid: remote_current,
new_oid: Some(src),
forced: false,
deletion: false,
status: PushRefStatus::UpToDate,
message: None,
},
action: PushAction::None,
apply: false,
});
}
let Some(old) = remote_current else {
return Ok(PushDecision {
result: PushRefResult {
local_ref: None,
remote_ref: spec.dst.clone(),
old_oid: None,
new_oid: Some(src),
forced: false,
deletion: false,
status: PushRefStatus::Ok,
message: None,
},
action: PushAction::Update(src),
apply: true,
});
};
let is_ff = local_repo
.map(|r| crate::merge_base::is_ancestor(r, old, src).unwrap_or(false))
.unwrap_or(false);
if is_ff {
Ok(PushDecision {
result: PushRefResult {
local_ref: None,
remote_ref: spec.dst.clone(),
old_oid: Some(old),
new_oid: Some(src),
forced: false,
deletion: false,
status: PushRefStatus::Ok,
message: None,
},
action: PushAction::Update(src),
apply: true,
})
} else if spec.force {
Ok(PushDecision {
result: PushRefResult {
local_ref: None,
remote_ref: spec.dst.clone(),
old_oid: Some(old),
new_oid: Some(src),
forced: true,
deletion: false,
status: PushRefStatus::Ok,
message: None,
},
action: PushAction::Update(src),
apply: true,
})
} else {
Ok(PushDecision {
result: PushRefResult {
local_ref: None,
remote_ref: spec.dst.clone(),
old_oid: Some(old),
new_oid: Some(src),
forced: false,
deletion: false,
status: PushRefStatus::RejectNonFastForward,
message: Some("non-fast-forward".to_owned()),
},
action: PushAction::None,
apply: false,
})
}
}
pub(crate) struct MatchedRef {
pub(crate) remote_ref: String,
pub(crate) local_ref: Option<String>,
pub(crate) oid: ObjectId,
pub(crate) force: bool,
pub(crate) is_tag: bool,
}
pub(crate) fn open_odb(git_dir: &Path) -> Odb {
Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.to_path_buf())
}
pub(crate) fn match_positive(refname: &str, positive: &[RefspecItem]) -> Option<Option<String>> {
for item in positive {
let Some(src) = item.src.as_deref() else {
continue;
};
if let Some(dst) = apply_refspec(src, item.dst.as_deref(), refname) {
if dst.is_empty() {
return Some(None);
}
return Some(Some(dst));
}
}
None
}
pub(crate) fn refspecs_force(refname: &str, positive: &[RefspecItem]) -> bool {
positive.iter().any(|item| {
item.force
&& item
.src
.as_deref()
.is_some_and(|src| apply_refspec(src, item.dst.as_deref(), refname).is_some())
})
}
pub(crate) fn ref_excluded(refname: &str, negatives: &[RefspecItem]) -> bool {
negatives.iter().any(|item| {
item.src
.as_deref()
.is_some_and(|src| glob_matches(src, refname))
})
}
fn apply_refspec(src: &str, dst: Option<&str>, refname: &str) -> Option<String> {
match src.find('*') {
Some(star) => {
let prefix = &src[..star];
let suffix = &src[star + 1..];
if !refname.starts_with(prefix)
|| !refname.ends_with(suffix)
|| refname.len() < prefix.len() + suffix.len()
{
return None;
}
let middle = &refname[prefix.len()..refname.len() - suffix.len()];
match dst {
None => Some(refname.to_owned()),
Some("") => Some(String::new()),
Some(d) => Some(d.replacen('*', middle, 1)),
}
}
None => {
if src != refname {
return None;
}
match dst {
None => Some(refname.to_owned()),
Some("") => Some(String::new()),
Some(d) => Some(d.to_owned()),
}
}
}
}
fn glob_matches(pattern: &str, refname: &str) -> bool {
match pattern.find('*') {
Some(star) => {
let prefix = &pattern[..star];
let suffix = &pattern[star + 1..];
refname.starts_with(prefix)
&& refname.ends_with(suffix)
&& refname.len() >= prefix.len() + suffix.len()
}
None => pattern == refname,
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn apply_tag_mode(
mode: TagMode,
remote_refs: &[(String, ObjectId)],
remote_odb: &Odb,
negatives: &[RefspecItem],
matched: &mut Vec<MatchedRef>,
matched_oids: &mut HashSet<ObjectId>,
seen_remote_ref: &mut HashSet<String>,
) -> Result<()> {
if mode == TagMode::None {
return Ok(());
}
let following_closure: HashSet<ObjectId> = if mode == TagMode::Following {
let roots: Vec<ObjectId> = matched.iter().map(|m| m.oid).collect();
reachable_closure(remote_odb, &roots, &HashSet::new(), true)?
} else {
HashSet::new()
};
for (name, oid) in remote_refs {
if !name.starts_with("refs/tags/") {
continue;
}
if seen_remote_ref.contains(name) || ref_excluded(name, negatives) {
continue;
}
let keep = match mode {
TagMode::All => true,
TagMode::Following => {
let peeled = peel_tag_target(remote_odb, *oid)?;
following_closure.contains(oid) || following_closure.contains(&peeled)
}
TagMode::None => false,
};
if keep {
seen_remote_ref.insert(name.clone());
matched_oids.insert(*oid);
matched.push(MatchedRef {
remote_ref: name.clone(),
local_ref: Some(name.clone()),
oid: *oid,
force: false,
is_tag: true,
});
}
}
Ok(())
}
fn peel_tag_target(odb: &Odb, oid: ObjectId) -> Result<ObjectId> {
let mut current = oid;
for _ in 0..16 {
let obj = match odb.read(¤t) {
Ok(o) => o,
Err(_) => return Ok(current),
};
if obj.kind != ObjectKind::Tag {
return Ok(current);
}
current = parse_tag(&obj.data)?.object;
}
Ok(current)
}
pub(crate) fn classify_update(
old: Option<&ObjectId>,
new: &ObjectId,
force: bool,
is_tag: bool,
repo: Option<&crate::repo::Repository>,
) -> UpdateMode {
let Some(old) = old else {
return UpdateMode::New;
};
if old == new {
return UpdateMode::UpToDate;
}
let ff = repo
.map(|r| crate::merge_base::is_ancestor(r, *old, *new).unwrap_or(false))
.unwrap_or(false);
if ff && !is_tag {
return UpdateMode::FastForward;
}
if force {
return UpdateMode::Forced;
}
if is_tag {
return UpdateMode::TagUpdateRejected;
}
UpdateMode::NonFastForwardRejected
}
pub(crate) fn prune_tracking_refs(
local_git_dir: &Path,
positive: &[RefspecItem],
remote_refs: &[(String, ObjectId)],
dry_run: bool,
updates: &mut Vec<RefUpdate>,
) -> Result<()> {
let mut live: HashSet<String> = HashSet::new();
for (name, _) in remote_refs {
if let Some(Some(dst)) = match_positive(name, positive) {
live.insert(dst);
}
}
let mut pruned: HashMap<String, ObjectId> = HashMap::new();
for item in positive {
let Some(dst) = item.dst.as_deref() else {
continue;
};
if let Some(star) = dst.find('*') {
let prefix = &dst[..star];
for (name, oid) in crate::refs::list_refs(local_git_dir, prefix)? {
if !name.starts_with(prefix) {
continue;
}
if !live.contains(&name) {
pruned.entry(name).or_insert(oid);
}
}
} else if !live.contains(dst) {
if let Ok(oid) = crate::refs::resolve_ref(local_git_dir, dst) {
pruned.entry(dst.to_owned()).or_insert(oid);
}
}
}
for (name, oid) in pruned {
if !dry_run {
crate::refs::delete_ref(local_git_dir, &name)?;
}
updates.push(RefUpdate {
remote_ref: String::new(),
local_ref: Some(name),
old_oid: Some(oid),
new_oid: None,
mode: UpdateMode::DeletedMissing,
note: Some("pruned (gone on remote)".to_owned()),
});
}
Ok(())
}