use crate::environment::RepositoryOptions;
use crate::partial_clone::ObjectFilterSpec;
use std::collections::{HashMap, HashSet, VecDeque};
use std::fs;
use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
use crate::fetch::{NoProgress, Progress};
use crate::hash;
use crate::objects::{parse_commit, parse_tag, parse_tree, HashAlgo, ObjectId, ObjectKind};
use crate::odb::Odb;
use crate::progress::{ProgressPhase, ProgressSink};
use crate::push_report::{PushRefResult, PushRefStatus};
use crate::refspec::{parse_fetch_refspec, RefspecItem};
use std::num::NonZeroU32;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UpdateMode {
New,
FastForward,
Forced,
UpToDate,
NoChangeNeeded,
NonFastForwardRejected,
TagUpdateRejected,
SourceObjectNotFound,
Unborn,
DeletedMissing,
}
impl UpdateMode {
#[must_use]
pub fn modified_repo(self) -> bool {
matches!(
self,
Self::New | Self::FastForward | Self::Forced | Self::DeletedMissing
)
}
#[must_use]
pub fn is_rejected(self) -> bool {
matches!(
self,
Self::NonFastForwardRejected | Self::TagUpdateRejected | Self::SourceObjectNotFound
)
}
#[must_use]
pub fn is_reported(self) -> bool {
self.modified_repo() || self.is_rejected()
}
}
#[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, Copy, Debug, PartialEq, Eq)]
pub enum FetchDepth {
Depth(NonZeroU32),
Deepen(NonZeroU32),
Unshallow,
}
impl FetchDepth {
#[must_use]
pub fn depth(n: u32) -> Option<Self> {
NonZeroU32::new(n).map(Self::Depth)
}
#[must_use]
pub fn deepen(n: u32) -> Option<Self> {
NonZeroU32::new(n).map(Self::Deepen)
}
}
pub struct FetchOptions {
pub refspecs: Vec<String>,
pub negative_refspecs: Vec<String>,
pub tags: TagMode,
pub prune: bool,
pub dry_run: bool,
pub fetch_depth: Option<FetchDepth>,
pub deepen_since: Option<String>,
pub deepen_not: Vec<String>,
pub initial_remote_fetch: bool,
pub remote_name: Option<String>,
pub fetch_source: Option<String>,
pub configured_refspecs: Vec<String>,
pub clone_reflog: Option<CloneReflog>,
pub diagnostics: Option<crate::diagnostics::DiagnosticsHandle>,
pub network_trace: bool,
pub index_pack_threads: Option<usize>,
pub thread_policy: crate::parallel::ThreadPolicy,
pub environment: crate::environment::Environment,
pub pack_spool_only: bool,
pub http_protocol_version: Option<u8>,
pub remote_progress: bool,
pub filter: Option<ObjectFilterSpec>,
pub mark_received_pack_promisor: bool,
}
impl Default for FetchOptions {
fn default() -> Self {
Self {
refspecs: Vec::new(),
negative_refspecs: Vec::new(),
tags: TagMode::Following,
prune: false,
dry_run: false,
fetch_depth: None,
deepen_since: None,
deepen_not: Vec::new(),
initial_remote_fetch: false,
remote_name: None,
fetch_source: None,
configured_refspecs: Vec::new(),
clone_reflog: None,
diagnostics: None,
network_trace: false,
index_pack_threads: None,
thread_policy: crate::parallel::ThreadPolicy::default(),
environment: crate::environment::Environment::empty(),
pack_spool_only: false,
http_protocol_version: None,
remote_progress: false,
filter: None,
mark_received_pack_promisor: false,
}
}
}
#[derive(Clone, Debug)]
pub struct CloneReflog {
pub identity: String,
pub message: String,
}
impl Clone for FetchOptions {
fn clone(&self) -> Self {
Self {
refspecs: self.refspecs.clone(),
negative_refspecs: self.negative_refspecs.clone(),
tags: self.tags,
prune: self.prune,
dry_run: self.dry_run,
fetch_depth: self.fetch_depth,
deepen_since: self.deepen_since.clone(),
deepen_not: self.deepen_not.clone(),
initial_remote_fetch: self.initial_remote_fetch,
remote_name: self.remote_name.clone(),
fetch_source: self.fetch_source.clone(),
configured_refspecs: self.configured_refspecs.clone(),
clone_reflog: self.clone_reflog.clone(),
diagnostics: self.diagnostics.clone(),
network_trace: self.network_trace,
index_pack_threads: self.index_pack_threads,
thread_policy: self.thread_policy,
environment: self.environment.clone(),
pack_spool_only: self.pack_spool_only,
http_protocol_version: self.http_protocol_version,
remote_progress: self.remote_progress,
filter: self.filter,
mark_received_pack_promisor: self.mark_received_pack_promisor,
}
}
}
impl FetchOptions {
#[must_use]
pub fn has_deepen_request(&self) -> bool {
self.fetch_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())
}
#[must_use]
pub(crate) fn wire_deepen(&self) -> Option<(u32, bool)> {
match self.fetch_depth {
Some(FetchDepth::Depth(n)) => Some((n.get(), false)),
Some(FetchDepth::Deepen(n)) => Some((n.get(), true)),
Some(FetchDepth::Unshallow) => Some((crate::shallow::INFINITE_DEPTH, false)),
None => None,
}
}
#[must_use]
pub(crate) fn absolute_depth_for_tags(&self) -> Option<u32> {
match self.fetch_depth {
Some(FetchDepth::Depth(n)) => Some(n.get()),
Some(FetchDepth::Deepen(_)) | Some(FetchDepth::Unshallow) | None => None,
}
}
}
pub fn validate_fetch_depth_local(local_git_dir: &Path, opts: &FetchOptions) -> Result<()> {
if matches!(opts.fetch_depth, Some(FetchDepth::Unshallow)) {
let local = crate::shallow::load_shallow_oids(local_git_dir)?;
if local.is_empty() {
return Err(Error::FetchUnshallowNotShallow);
}
}
Ok(())
}
pub(crate) fn validate_fetch_depth_server(
opts: &FetchOptions,
v0_caps: Option<&str>,
v2_caps: Option<&[String]>,
) -> Result<()> {
let needs_shallow = opts.has_deepen_request()
|| opts.wire_deepen().is_some()
|| opts
.deepen_since
.as_deref()
.is_some_and(|s| !s.trim().is_empty())
|| opts.deepen_not.iter().any(|s| !s.trim().is_empty());
if !needs_shallow {
return Ok(());
}
if let Some(caps) = v0_caps {
if !caps.contains("shallow") {
return Err(Error::FetchShallowUnsupported);
}
if opts.wire_deepen().is_some_and(|(_, rel)| rel) && !caps.contains("deepen-relative") {
return Err(Error::FetchDeepenRelativeUnsupported);
}
} else if let Some(caps) = v2_caps {
if !crate::protocol_v2::fetch_supports_feature(caps, "shallow") {
return Err(Error::FetchShallowUnsupported);
}
}
Ok(())
}
pub fn resolve_fetch_prune(
config: &crate::config::ConfigSet,
remote: &str,
cli_prune: Option<bool>,
) -> Result<bool> {
if let Some(enabled) = cli_prune {
return Ok(enabled);
}
let remote_key = format!("remote.{remote}.prune");
if let Some(value) = optional_config_bool(config, &remote_key)? {
return Ok(value);
}
if let Some(value) = optional_config_bool(config, "fetch.prune")? {
return Ok(value);
}
Ok(false)
}
#[must_use]
pub fn resolve_fetch_tags(
config: &crate::config::ConfigSet,
remote: &str,
cli_tags: Option<bool>,
) -> TagMode {
if let Some(all) = cli_tags {
return if all { TagMode::All } else { TagMode::None };
}
let key = format!("remote.{remote}.tagOpt");
match config.get(&key).as_deref().map(str::trim) {
Some("--tags") | Some("tags") | Some("all") => TagMode::All,
Some("--no-tags") | Some("no-tags") | Some("no") | Some("never") => TagMode::None,
_ => TagMode::Following,
}
}
fn optional_config_bool(config: &crate::config::ConfigSet, key: &str) -> Result<Option<bool>> {
match config.get_bool(key) {
None => Ok(None),
Some(Ok(value)) => Ok(Some(value)),
Some(Err(message)) => Err(Error::Config(crate::error::ConfigError::Other(format!(
"invalid boolean for '{key}': {message}"
)))),
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TransferStats {
pub objects: u64,
pub bytes: u64,
pub kinds: Option<crate::index_pack::ObjectKindCounts>,
}
impl TransferStats {
pub fn from_ingested_pack(ingested: &crate::index_pack::IngestedPack) -> Result<Self> {
transfer_stats_from_ingested(ingested)
}
}
#[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>,
pub transfer: TransferStats,
}
impl FetchOutcome {
#[must_use]
pub fn branch_count(&self) -> u64 {
self.updates
.iter()
.filter(|u| u.remote_ref.starts_with("refs/heads/"))
.count() as u64
}
#[must_use]
pub fn tag_count(&self) -> u64 {
self.updates
.iter()
.filter(|u| u.remote_ref.starts_with("refs/tags/") && !u.remote_ref.ends_with("^{}"))
.count() as u64
}
pub fn reported_updates(&self) -> impl Iterator<Item = &RefUpdate> {
self.updates.iter().filter(|u| u.mode.is_reported())
}
#[must_use]
pub fn changed_ref_count(&self) -> usize {
self.updates
.iter()
.filter(|u| u.mode.modified_repo())
.count()
}
#[must_use]
pub fn has_rejections(&self) -> bool {
self.updates.iter().any(|u| u.mode.is_rejected())
}
}
fn transfer_stats_from_ingested(
ingested: &crate::index_pack::IngestedPack,
) -> Result<TransferStats> {
let bytes = fs::metadata(&ingested.pack_path).map_err(Error::Io)?.len();
Ok(TransferStats {
objects: u64::try_from(ingested.object_ids.len()).unwrap_or(u64::MAX),
bytes,
kinds: Some(ingested.object_counts),
})
}
fn object_kind_counts_for_oids(
odb: &Odb,
oids: &HashSet<ObjectId>,
) -> Result<crate::index_pack::ObjectKindCounts> {
use crate::index_pack::ObjectKindCounts;
let mut kinds = ObjectKindCounts::default();
for oid in oids {
kinds.add(odb.read_info(oid)?.kind);
}
Ok(kinds)
}
fn loose_transfer_stats(objects_dir: &Path, oids: &HashSet<ObjectId>) -> Result<(u64, u64)> {
let mut objects = 0u64;
let mut bytes = 0u64;
for oid in oids {
let path = oid.loose_path_in(objects_dir);
if path.is_file() {
objects += 1;
bytes += fs::metadata(&path).map_err(Error::Io)?.len();
}
}
Ok((objects, bytes))
}
fn pack_file_basenames(pack_dir: &Path) -> HashSet<String> {
let mut names = HashSet::new();
let Ok(entries) = fs::read_dir(pack_dir) else {
return names;
};
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "pack") {
if let Some(name) = path.file_name() {
names.insert(name.to_string_lossy().into_owned());
}
}
}
names
}
fn transfer_stats_for_local_operation(
local_git_dir: &Path,
local_odb: &Odb,
installed: &HashSet<ObjectId>,
pack_dir_before: &HashSet<String>,
) -> Result<TransferStats> {
let objects_dir = local_git_dir.join("objects");
let pack_dir = local_objects_pack_dir(local_git_dir);
let mut bytes = 0u64;
let mut objects_from_new_packs = 0u64;
if pack_dir.is_dir() {
for entry in fs::read_dir(&pack_dir).map_err(Error::Io)? {
let path = entry.map_err(Error::Io)?.path();
if path.extension().is_none_or(|e| e != "pack") {
continue;
}
let Some(name) = path.file_name() else {
continue;
};
let name = name.to_string_lossy();
if pack_dir_before.contains(name.as_ref()) {
continue;
}
bytes += fs::metadata(&path).map_err(Error::Io)?.len();
let idx = path.with_extension("idx");
if idx.is_file() {
let index = crate::pack::read_pack_index_no_verify(&idx)?;
objects_from_new_packs += u64::try_from(index.len()).unwrap_or(u64::MAX);
}
}
}
let (loose_objects, loose_bytes) = loose_transfer_stats(&objects_dir, installed)?;
bytes = bytes.saturating_add(loose_bytes);
let installed_count = u64::try_from(installed.len()).unwrap_or(u64::MAX);
let objects = if objects_from_new_packs > 0 {
objects_from_new_packs.saturating_add(loose_objects)
} else {
installed_count
};
let kinds = if objects == installed_count {
Some(object_kind_counts_for_oids(local_odb, installed)?)
} else {
None
};
Ok(TransferStats {
objects,
bytes,
kinds,
})
}
fn local_objects_pack_dir(git_dir: &Path) -> PathBuf {
git_dir.join("objects").join("pack")
}
#[derive(Clone, Debug, Default)]
pub struct PushRefSpec {
pub src: Option<ObjectId>,
pub src_ref: Option<String>,
pub dst: String,
pub force: bool,
pub delete: bool,
pub expected_old: Option<ObjectId>,
pub expect_absent: bool,
}
#[must_use]
pub(crate) fn push_spec_local_ref(spec: &PushRefSpec) -> Option<String> {
spec.src_ref.clone()
}
#[must_use]
pub fn resolve_push_delete_ref(
git_dir: &Path,
remote_ref_names: &HashSet<String>,
spec: &str,
) -> String {
if spec.starts_with("refs/") {
return spec.to_owned();
}
let branch = format!("refs/heads/{spec}");
let tag = format!("refs/tags/{spec}");
if crate::refs::resolve_ref(git_dir, &branch).is_ok() {
return branch;
}
if remote_ref_names.contains(&branch) {
return branch;
}
if remote_ref_names.contains(&tag) {
return tag;
}
branch
}
#[derive(Clone, Default)]
pub struct PushOptions {
pub atomic: bool,
pub dry_run: bool,
pub tracking_remote: Option<String>,
pub push_options: Vec<String>,
pub diagnostics: Option<crate::diagnostics::DiagnosticsHandle>,
pub network_trace: bool,
pub remote_ref_tips: Option<Vec<ObjectId>>,
}
#[derive(Clone, Debug, Default)]
pub struct PushOutcome {
pub results: Vec<PushRefResult>,
}
pub use crate::pack_objects::{
build_pack, build_pack_from_send_list, reachable_closure, PackBuildOptions, PackObjects,
PackObjectsOptions, PackStats,
};
pub(crate) fn collect_objects_for_commits_only(
odb: &Odb,
commits: &[ObjectId],
stop: Option<&HashSet<ObjectId>>,
) -> 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 stop.is_some_and(|s| s.contains(&oid)) {
return false;
}
if visited.insert(oid) {
ordered.push(oid);
queue.push_back(oid);
true
} else {
false
}
};
for &c in commits {
enqueue(c, &mut queue, &mut visited, &mut ordered);
}
while let Some(oid) = queue.pop_front() {
let obj = odb.read(&oid)?;
match obj.kind {
ObjectKind::Commit => {
let commit = parse_commit(&obj.data)?;
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 merge_annotated_tag_wants(
odb: &Odb,
wants: &[ObjectId],
send: &mut Vec<ObjectId>,
) -> Result<()> {
let mut present: HashSet<ObjectId> = send.iter().copied().collect();
for &want in wants {
if present.contains(&want) {
continue;
}
if odb.read(&want)?.kind == ObjectKind::Tag {
send.push(want);
present.insert(want);
}
}
Ok(())
}
pub(crate) fn build_pack_push_fast(
local_git_dir: &Path,
odb: &Odb,
wants: &[ObjectId],
haves: &[ObjectId],
opts: &PackBuildOptions,
) -> Result<Vec<u8>> {
use crate::merge_base::{walk_commits_reachable_excluding_ancestors_of, ReachableWalkLimit};
let hide: Vec<ObjectId> = haves
.iter()
.copied()
.filter(|tip| odb.exists(tip))
.collect();
let repo = crate::repo::Repository::open(local_git_dir, &RepositoryOptions::empty())?;
let mut commit_set: HashSet<ObjectId> = HashSet::new();
for &want in wants {
let (_, commits) = walk_commits_reachable_excluding_ancestors_of(
&repo,
&[want],
&hide,
ReachableWalkLimit::Unlimited,
)?;
commit_set.extend(commits);
}
let commits: Vec<ObjectId> = commit_set.into_iter().collect();
let mut exclude: HashSet<ObjectId> = HashSet::new();
for &tip in &hide {
exclude.insert(tip);
let obj = odb.read(&tip)?;
if obj.kind == ObjectKind::Commit {
let commit = parse_commit(&obj.data)?;
exclude.insert(commit.tree);
}
}
let empty_haves = HashSet::new();
let empty_name_hashes: HashMap<ObjectId, u32> = HashMap::new();
collect_objects_for_commits_only(odb, &commits, Some(&exclude)).and_then(|mut send| {
merge_annotated_tag_wants(odb, wants, &mut send)?;
build_pack_from_send_list(odb, &send, &empty_haves, opts, &empty_name_hashes)
})
}
pub(crate) use crate::pack_objects::build_pack_for_local_fetch;
fn pack_build_options_for_send(
source_odb: &Odb,
oids: &[ObjectId],
base: &PackBuildOptions,
) -> PackBuildOptions {
if oids.is_empty() || oids.len() > 512 {
return *base;
}
let commits_tags_only = oids.iter().all(|oid| {
source_odb
.read(oid)
.map(|obj| matches!(obj.kind, ObjectKind::Commit | ObjectKind::Tag))
.unwrap_or(false)
});
if !commits_tags_only {
return *base;
}
let mut fast = *base;
fast.delta = false;
fast.reuse_deltas = false;
fast.thin = false;
fast
}
fn ingest_pack_options_for_fetch(
local_git_dir: &Path,
fetch_opts: &FetchOptions,
) -> Result<crate::index_pack::IngestPackOptions> {
let pack_config =
crate::config::ConfigSet::load(&fetch_opts.environment, Some(local_git_dir), true)?;
let index_threads = crate::parallel::index_pack_threads_for_ingest(
fetch_opts.thread_policy,
&pack_config,
fetch_opts.index_pack_threads,
)?;
Ok(crate::index_pack::IngestPackOptions {
fix_thin: true,
skip_post_index_verify: true,
threads: index_threads,
merged_config: Some(pack_config),
environment: fetch_opts.environment.clone(),
mark_promisor_pack: fetch_opts.mark_received_pack_promisor,
})
}
#[allow(clippy::too_many_arguments)]
fn copy_objects_for_local_fetch(
remote_git_dir: &Path,
local_git_dir: &Path,
remote_odb: &Odb,
local_odb: &Odb,
wants: &[ObjectId],
haves: &[ObjectId],
source_shallow: &HashSet<ObjectId>,
have_shallow: &HashSet<ObjectId>,
try_pack_link: bool,
pack_build: &PackBuildOptions,
fetch_opts: &FetchOptions,
progress: &mut dyn ProgressSink,
) -> Result<(HashSet<ObjectId>, TransferStats)> {
use crate::local_object_copy::{
link_loose_objects_for_oids, linked_object_set, refresh_local_odb_after_link,
source_objects_fully_packed, try_satisfy_via_object_link,
};
let remote_objects = remote_git_dir.join("objects");
let local_objects = local_git_dir.join("objects");
let pack_dir_before = pack_file_basenames(&local_objects_pack_dir(local_git_dir));
if !pack_build.thin
&& try_pack_link
&& haves.is_empty()
&& !wants.is_empty()
&& source_objects_fully_packed(&remote_objects)?
{
progress.start(ProgressPhase::CopyingObjects, Some(wants.len() as u64));
if try_satisfy_via_object_link(&remote_objects, &local_objects, local_odb, wants, false)? {
progress.set(wants.len() as u64);
progress.finish();
let oids = linked_object_set(local_odb, wants);
let transfer = transfer_stats_for_local_operation(
local_git_dir,
local_odb,
&oids,
&pack_dir_before,
)?;
return Ok((oids, transfer));
}
progress.finish();
}
let have_tips: HashSet<ObjectId> = haves.iter().copied().collect();
let send_set = collect_reachable_excluding_peer(
remote_odb,
wants,
local_odb,
&have_tips,
false,
source_shallow,
)?;
let send = &send_set.objects;
if !pack_build.thin && try_pack_link {
progress.start(ProgressPhase::CopyingObjects, Some(send.len() as u64));
if try_satisfy_via_object_link(&remote_objects, &local_objects, local_odb, send, false)? {
progress.set(send.len() as u64);
progress.finish();
let oids = linked_object_set(local_odb, send);
let transfer = transfer_stats_for_local_operation(
local_git_dir,
local_odb,
&oids,
&pack_dir_before,
)?;
return Ok((oids, transfer));
}
progress.finish();
}
let mut still_missing = objects_missing_from_peer(local_odb, send);
if still_missing.is_empty() {
let oids = linked_object_set(local_odb, send);
let transfer =
transfer_stats_for_local_operation(local_git_dir, local_odb, &oids, &pack_dir_before)?;
return Ok((oids, transfer));
}
if !haves.is_empty() {
progress.start(ProgressPhase::CopyingObjects, Some(send.len() as u64));
let linked_loose =
link_loose_objects_for_oids(&remote_objects, &local_objects, &still_missing)?;
if linked_loose > 0 {
progress.inc(linked_loose as u64);
refresh_local_odb_after_link(local_odb);
still_missing.retain(|oid| !local_odb.exists(oid));
}
progress.finish();
if still_missing.is_empty() {
let oids = linked_object_set(local_odb, send);
let transfer = transfer_stats_for_local_operation(
local_git_dir,
local_odb,
&oids,
&pack_dir_before,
)?;
return Ok((oids, transfer));
}
}
let pack_opts = pack_build_options_for_send(remote_odb, &still_missing, pack_build);
progress.start(
ProgressPhase::CopyingObjects,
Some(still_missing.len() as u64),
);
let pack = build_pack_for_local_transfer(
remote_odb,
&send_set,
&still_missing,
local_odb,
haves,
source_shallow,
have_shallow,
&pack_opts,
)?;
let ingest_opts = ingest_pack_options_for_fetch(local_git_dir, fetch_opts)?;
let ingested = crate::index_pack::ingest_received_pack(pack, local_odb, &ingest_opts)?;
progress.inc(ingested.object_ids.len() as u64);
progress.finish();
refresh_local_odb_after_link(local_odb);
let mut out = linked_object_set(local_odb, send);
out.extend(ingested.object_ids);
let transfer =
transfer_stats_for_local_operation(local_git_dir, local_odb, &out, &pack_dir_before)?;
Ok((out, transfer))
}
#[allow(clippy::too_many_arguments)]
fn copy_objects_for_local_push(
local_git_dir: &Path,
remote_git_dir: &Path,
local_odb: &Odb,
remote_odb: &Odb,
wants: &[ObjectId],
haves: &[ObjectId],
source_shallow: &HashSet<ObjectId>,
have_shallow: &HashSet<ObjectId>,
opts: &PackBuildOptions,
) -> Result<()> {
use crate::local_object_copy::{
link_loose_objects_for_oids, refresh_local_odb_after_link, try_satisfy_via_object_link,
};
let local_objects = local_git_dir.join("objects");
let remote_objects = remote_git_dir.join("objects");
let have_tips: HashSet<ObjectId> = haves.iter().copied().collect();
let send_set = collect_reachable_excluding_peer(
local_odb,
wants,
remote_odb,
&have_tips,
false,
source_shallow,
)?;
let send = &send_set.objects;
if !opts.thin
&& try_satisfy_via_object_link(&local_objects, &remote_objects, remote_odb, send, false)?
{
return Ok(());
}
let mut still_missing = objects_missing_from_peer(remote_odb, send);
if still_missing.is_empty() {
return Ok(());
}
if !haves.is_empty() {
let linked_loose =
link_loose_objects_for_oids(&local_objects, &remote_objects, &still_missing)?;
if linked_loose > 0 {
refresh_local_odb_after_link(remote_odb);
still_missing.retain(|oid| !remote_odb.exists(oid));
}
if still_missing.is_empty() {
return Ok(());
}
}
let pack_opts = pack_build_options_for_send(local_odb, &still_missing, opts);
let pack = build_pack_for_local_transfer(
local_odb,
&send_set,
&still_missing,
remote_odb,
haves,
source_shallow,
have_shallow,
&pack_opts,
)?;
crate::index_pack::ingest_received_pack(
pack,
remote_odb,
&crate::index_pack::IngestPackOptions {
fix_thin: pack_opts.thin,
..Default::default()
},
)?;
refresh_local_odb_after_link(remote_odb);
Ok(())
}
fn objects_missing_from_peer(peer_odb: &Odb, send: &[ObjectId]) -> Vec<ObjectId> {
peer_odb.reprepare_local_packs();
send.iter()
.copied()
.filter(|oid| !peer_odb.exists_materialized_local_without_rescan(oid))
.collect()
}
#[allow(clippy::too_many_arguments)]
fn build_pack_for_local_transfer(
source_odb: &Odb,
send_set: &PeerSendSet,
missing: &[ObjectId],
peer_odb: &Odb,
haves: &[ObjectId],
source_shallow: &HashSet<ObjectId>,
have_shallow: &HashSet<ObjectId>,
opts: &PackBuildOptions,
) -> Result<Vec<u8>> {
if haves.is_empty() && !opts.thin {
return crate::pack_objects::build_pack_from_send_list(
source_odb,
missing,
&HashSet::new(),
opts,
&send_set.blob_name_hashes,
);
}
build_pack_for_local_fetch(
source_odb,
missing,
peer_odb,
haves,
source_shallow,
have_shallow,
opts,
)
}
struct PeerSendSet {
objects: Vec<ObjectId>,
blob_name_hashes: HashMap<ObjectId, u32>,
}
fn collect_reachable_excluding_peer(
source_odb: &Odb,
roots: &[ObjectId],
peer_odb: &Odb,
peer_boundary: &HashSet<ObjectId>,
skip_missing: bool,
shallow_grafts: &HashSet<ObjectId>,
) -> Result<PeerSendSet> {
use crate::pack_name_hash::pack_name_hash_extend;
let mut visited: HashSet<ObjectId> = HashSet::new();
let mut ordered: Vec<ObjectId> = Vec::new();
let mut blob_name_hashes: HashMap<ObjectId, u32> = HashMap::new();
let mut queue: VecDeque<(ObjectId, u32)> = VecDeque::new();
peer_odb.reprepare_local_packs();
let at_boundary = |oid: ObjectId| {
peer_boundary.contains(&oid) || peer_odb.exists_materialized_local_without_rescan(&oid)
};
let enqueue = |oid: ObjectId,
prefix_hash: Option<u32>,
queue: &mut VecDeque<(ObjectId, u32)>,
visited: &mut HashSet<ObjectId>,
ordered: &mut Vec<ObjectId>|
-> bool {
if !visited.insert(oid) || at_boundary(oid) {
return false;
}
ordered.push(oid);
if let Some(prefix_hash) = prefix_hash {
queue.push_back((oid, prefix_hash));
}
true
};
for &root in roots {
enqueue(root, Some(0), &mut queue, &mut visited, &mut ordered);
}
while let Some((oid, prefix_hash)) = queue.pop_front() {
let obj = match source_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)?;
if !shallow_grafts.contains(&oid) {
for parent in commit.parents {
enqueue(parent, Some(0), &mut queue, &mut visited, &mut ordered);
}
}
enqueue(commit.tree, Some(0), &mut queue, &mut visited, &mut ordered);
}
ObjectKind::Tree => {
for entry in parse_tree(&obj.data)? {
if entry.mode == 0o160000 {
continue;
}
let path_hash = pack_name_hash_extend(prefix_hash, &entry.name);
if entry.mode & 0o170000 == 0o040000 {
let dir_hash = pack_name_hash_extend(path_hash, b"/");
enqueue(
entry.oid,
Some(dir_hash),
&mut queue,
&mut visited,
&mut ordered,
);
} else if enqueue(entry.oid, None, &mut queue, &mut visited, &mut ordered) {
blob_name_hashes.insert(entry.oid, path_hash);
}
}
}
ObjectKind::Tag => {
let tag = parse_tag(&obj.data)?;
enqueue(
tag.object,
Some(prefix_hash),
&mut queue,
&mut visited,
&mut ordered,
);
}
ObjectKind::Blob => {}
}
}
Ok(PeerSendSet {
objects: ordered,
blob_name_hashes,
})
}
pub(crate) fn fix_thin_pack_path(pack_path: &Path, odb: &Odb) -> Result<std::path::PathBuf> {
use crate::pack_map::PackData;
use std::ops::Deref;
let mapped = PackData::open(pack_path)?;
if !crate::unpack_objects::pack_is_thin(mapped.deref(), odb.hash_algo()) {
return Ok(pack_path.to_path_buf());
}
let data = std::fs::read(pack_path).map_err(Error::Io)?;
let fixed = fix_thin_pack(data, odb)?;
let out = pack_path.with_extension("fixed");
std::fs::write(&out, &fixed).map_err(Error::Io)?;
let _ = std::fs::remove_file(pack_path);
Ok(out)
}
pub(crate) fn fix_thin_pack(mut pack: Vec<u8>, odb: &Odb) -> Result<Vec<u8>> {
let algo = odb.hash_algo();
let hb = algo.len();
if !crate::unpack_objects::pack_is_thin(&pack, algo) {
return Ok(pack);
}
if pack.len() < 12 + hb {
return Err(Error::CorruptObject(
"thin pack fix: pack too small".to_owned(),
));
}
let mut missing: Vec<ObjectId> = Vec::new();
let mut seen = HashSet::new();
let in_pack = in_pack_whole_object_ids(&pack, algo)?;
let mut pos = 12usize;
let pack_end = pack.len() - hb;
while pos < pack_end {
let (type_code, size, header_len) = read_pack_type_size_at(&pack, pos)?;
let payload_start = pos + header_len;
match type_code {
7 => {
if payload_start + hb > pack_end {
return Err(Error::CorruptObject(
"thin pack fix: truncated ref-delta".to_owned(),
));
}
let base_oid = ObjectId::from_bytes(&pack[payload_start..payload_start + hb])?;
if !in_pack.contains(&base_oid) && seen.insert(base_oid) {
missing.push(base_oid);
}
pos = payload_start + hb + zlib_skip(&pack[payload_start + hb..], size)?;
}
6 => {
let (base_len, _) = read_ofs_delta_prefix_len(&pack[payload_start..])?;
pos =
payload_start + base_len + zlib_skip(&pack[payload_start + base_len..], size)?;
}
1..=4 => {
pos = payload_start + zlib_skip(&pack[payload_start..], size)?;
}
_ => {
return Err(Error::CorruptObject(format!(
"thin pack fix: unknown type {type_code}"
)));
}
}
}
if missing.is_empty() {
return Ok(pack);
}
pack.truncate(pack.len() - hb);
crate::pack_objects::append_whole_objects_from_odb(&mut pack, odb, &missing)?;
let old_count = u32::from_be_bytes(
pack[8..12]
.try_into()
.map_err(|_| Error::CorruptObject("thin pack fix: bad object count".to_owned()))?,
);
let added = u32::try_from(missing.len())
.map_err(|_| Error::CorruptObject("thin pack fix: object count overflow".to_owned()))?;
let new_count = old_count
.checked_add(added)
.ok_or_else(|| Error::CorruptObject("thin pack fix: object count overflow".to_owned()))?;
pack[8..12].copy_from_slice(&new_count.to_be_bytes());
crate::pack_objects::append_pack_trailer(&mut pack, algo);
Ok(pack)
}
fn in_pack_whole_object_ids(pack: &[u8], algo: HashAlgo) -> Result<HashSet<ObjectId>> {
let mut out = HashSet::new();
let hb = algo.len();
if pack.len() < 12 + hb {
return Ok(out);
}
let pack_end = pack.len() - hb;
let mut pos = 12usize;
while pos < pack_end {
let Ok((type_code, size, header_len)) = read_pack_type_size_at(pack, pos) else {
break;
};
let payload_start = pos + header_len;
if (1..=4).contains(&type_code) {
if let Ok(data) = zlib_decompress_fixed(&pack[payload_start..], size) {
if let Ok(kind) = pack_type_code_to_kind(type_code) {
let oid = hash::hash_object(algo, kind, &data)?;
out.insert(oid);
}
}
}
let advance = match type_code {
7 if payload_start + hb <= pack_end => {
header_len + hb + zlib_skip(&pack[payload_start + hb..], size).unwrap_or(0)
}
6 => {
let base_len = read_ofs_delta_prefix_len(&pack[payload_start..])
.map(|(n, _)| n)
.unwrap_or(0);
header_len
+ base_len
+ zlib_skip(&pack[payload_start + base_len..], size).unwrap_or(0)
}
1..=4 => header_len + zlib_skip(&pack[payload_start..], size).unwrap_or(0),
_ => break,
};
pos += advance;
}
Ok(out)
}
fn pack_type_code_to_kind(code: u8) -> Result<ObjectKind> {
match code {
1 => Ok(ObjectKind::Commit),
2 => Ok(ObjectKind::Tree),
3 => Ok(ObjectKind::Blob),
4 => Ok(ObjectKind::Tag),
_ => Err(Error::CorruptObject(format!("bad pack type {code}"))),
}
}
fn read_pack_type_size_at(pack: &[u8], start: usize) -> Result<(u8, usize, usize)> {
let first = *pack
.get(start)
.ok_or_else(|| Error::CorruptObject("truncated pack header".to_owned()))?;
let type_code = (first >> 4) & 0x7;
let mut size = (first & 0x0f) as usize;
let mut pos = start + 1;
let mut shift = 4u32;
let mut cur = first;
while cur & 0x80 != 0 {
cur = *pack
.get(pos)
.ok_or_else(|| Error::CorruptObject("truncated pack header".to_owned()))?;
pos += 1;
size |= ((cur & 0x7f) as usize) << shift;
shift += 7;
}
Ok((type_code, size, pos - start))
}
fn read_ofs_delta_prefix_len(bytes: &[u8]) -> Result<(usize, u64)> {
let mut pos = 0usize;
let mut c = *bytes
.get(pos)
.ok_or_else(|| Error::CorruptObject("truncated ofs-delta".to_owned()))?;
pos += 1;
let mut value = (c & 0x7f) as u64;
while c & 0x80 != 0 {
c = *bytes
.get(pos)
.ok_or_else(|| Error::CorruptObject("truncated ofs-delta".to_owned()))?;
pos += 1;
value = (value + 1) << 7 | (c & 0x7f) as u64;
}
Ok((pos, value))
}
fn zlib_skip(bytes: &[u8], expected_size: usize) -> Result<usize> {
use flate2::read::ZlibDecoder;
use std::io::Read;
let mut dec = ZlibDecoder::new(bytes);
let mut tmp = Vec::with_capacity(expected_size);
dec.read_to_end(&mut tmp)
.map_err(|e| Error::Zlib(e.to_string()))?;
Ok(dec.total_in() as usize)
}
fn zlib_decompress_fixed(bytes: &[u8], expected_size: usize) -> Result<Vec<u8>> {
use flate2::read::ZlibDecoder;
use std::io::Read;
let mut dec = ZlibDecoder::new(bytes);
let mut out = Vec::with_capacity(expected_size);
dec.read_to_end(&mut out)
.map_err(|e| Error::Zlib(e.to_string()))?;
if out.len() != expected_size {
return Err(Error::CorruptObject(format!(
"zlib size mismatch: got {} expected {expected_size}",
out.len()
)));
}
Ok(out)
}
pub fn fetch_local(
local_git_dir: &Path,
remote_git_dir: &Path,
opts: &FetchOptions,
) -> Result<FetchOutcome> {
fetch_local_with_progress(local_git_dir, remote_git_dir, opts, &mut NoProgress)
}
pub(crate) struct FetchMatchSet {
pub(crate) matched: Vec<MatchedRef>,
pub(crate) positive: Vec<RefspecItem>,
pub(crate) negatives: Vec<RefspecItem>,
pub(crate) explicit_refspec_fetch: bool,
}
pub(crate) fn build_fetch_matched_set(
opts: &FetchOptions,
remote_refs: &[(String, ObjectId)],
remote_head_oid: Option<ObjectId>,
) -> Result<FetchMatchSet> {
use crate::fetch_request::{FetchRefspecInput, FetchRefspecPlan, FetchRemoteAdvertisement};
let remote = FetchRemoteAdvertisement {
refs: remote_refs,
head_oid: remote_head_oid,
};
let plan = FetchRefspecPlan::resolve(
&FetchRefspecInput {
user_refspecs: opts.refspecs.clone(),
configured_refspecs: opts.configured_refspecs.clone(),
remote_name: opts.remote_name.clone(),
allow_missing_explicit_for_prune: opts.prune,
},
remote,
)?;
let explicit_refspec_fetch = plan.is_explicit();
let mut negatives = match &plan {
FetchRefspecPlan::Pattern { negatives, .. } | FetchRefspecPlan::Mixed { negatives, .. } => {
negatives.clone()
}
};
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 positive = match &plan {
FetchRefspecPlan::Pattern { positive, .. } => positive.clone(),
FetchRefspecPlan::Mixed {
pattern_positive, ..
} => {
let mut positive = pattern_positive.clone();
for spec in &opts.refspecs {
if let Ok(item) = parse_fetch_refspec(spec) {
if !item.negative && !item.pattern && !item.matching {
positive.push(item);
}
}
}
positive
}
};
if opts.prune {
crate::fetch_request::supplement_positive_for_explicit_prune(
&mut positive,
&opts.configured_refspecs,
opts.remote_name.as_deref(),
);
}
let matched =
crate::fetch_request::matched_refs_with_negatives(&plan, remote, &negatives, opts.prune)?;
Ok(FetchMatchSet {
matched,
positive,
negatives,
explicit_refspec_fetch,
})
}
pub(crate) fn enforce_fetch_ref_update(
local_ref: &str,
old: Option<ObjectId>,
new: ObjectId,
mode: UpdateMode,
opts: &FetchOptions,
explicit_refspec_fetch: bool,
local_repo: Option<&crate::repo::Repository>,
) -> Result<()> {
if opts.dry_run {
return Ok(());
}
let would_write = matches!(
mode,
UpdateMode::New | UpdateMode::FastForward | UpdateMode::Forced
);
if let Some(short) = local_ref.strip_prefix("refs/heads/") {
if old.is_some()
&& old != Some(new)
&& (would_write || mode == UpdateMode::NonFastForwardRejected)
{
if let Some(repo) = local_repo {
if crate::worktree::find_worktree_with_branch_checked_out(repo, short, None)?
.is_some()
{
return Err(Error::FetchIntoCheckedOutBranch {
branch: short.to_owned(),
});
}
}
}
}
let _ = explicit_refspec_fetch;
Ok(())
}
pub fn fetch_local_with_progress(
local_git_dir: &Path,
remote_git_dir: &Path,
opts: &FetchOptions,
progress: &mut dyn Progress,
) -> Result<FetchOutcome> {
if !remote_git_dir.join("objects").is_dir() {
return Err(Error::Message(format!(
"could not find repository at '{}'",
remote_git_dir.display()
)));
}
validate_fetch_depth_local(local_git_dir, opts)?;
let local_odb = open_odb(local_git_dir);
let remote_odb = open_odb(remote_git_dir);
let remote_entries = crate::remote::list_refs_from_git_dir_for_fetch(
remote_git_dir,
Some(&remote_odb),
&opts.refspecs,
)
.map_err(crate::error::Error::from)?;
let mut default_branch = None;
let mut remote_head_oid = None;
let mut remote_refs: Vec<(String, ObjectId)> = Vec::new();
for entry in &remote_entries {
if entry.name == "HEAD" {
remote_head_oid = Some(entry.oid);
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;
}
if !crate::refs::is_valid_fetch_advertised_ref(&entry.name) {
continue;
}
remote_refs.push((entry.name.clone(), entry.oid));
}
let FetchMatchSet {
mut matched,
positive,
negatives,
explicit_refspec_fetch,
} = build_fetch_matched_set(opts, &remote_refs, remote_head_oid)?;
let mut matched_oids: HashSet<ObjectId> = matched.iter().map(|m| m.oid).collect();
let mut seen_remote_ref: HashSet<String> =
matched.iter().map(|m| m.remote_ref.clone()).collect();
let local_shallow = crate::shallow::load_shallow_boundaries(local_git_dir);
let remote_shallow = crate::shallow::load_shallow_boundaries(remote_git_dir);
let mut tag_shallow = local_shallow.clone();
tag_shallow.extend(remote_shallow.iter().copied());
let mut following_only = HashSet::new();
if matches!(opts.tags, TagMode::All | TagMode::Following) {
let tags_present = if opts.tags == TagMode::Following {
crate::fetch::tags_already_local(local_git_dir, &remote_refs)
} else {
HashSet::new()
};
following_only = apply_tag_mode(
opts.tags,
&remote_refs,
&remote_odb,
&tag_shallow,
&negatives,
&tags_present,
&mut matched,
&mut matched_oids,
&mut seen_remote_ref,
)?;
}
let fetch_commit_oids: HashSet<ObjectId> = matched
.iter()
.filter(|m| !m.is_tag)
.map(|m| m.oid)
.collect();
let mut following_tag_wants =
crate::fetch::following_tag_wants_with_local_peel(&matched, &following_only, &local_odb);
let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
if shallow_request {
let peel_set = crate::fetch::following_peel_commit_set_for_shallow(
&remote_odb,
&fetch_commit_oids,
opts.absolute_depth_for_tags(),
&tag_shallow,
);
following_tag_wants.extend(
crate::fetch::following_tag_wants_peel_among_fetched_commits(
&matched,
&following_only,
&peel_set,
),
);
}
let mut wants: Vec<ObjectId> = if shallow_request {
matched_oids
.iter()
.copied()
.filter(|oid| !following_only.contains(oid) || following_tag_wants.contains(oid))
.collect()
} else {
matched_oids
.iter()
.copied()
.filter(|oid| !local_odb.exists(oid))
.collect()
};
let shallow_wants: Vec<ObjectId> = matched
.iter()
.filter(|m| !m.is_tag)
.map(|m| m.oid)
.filter(|oid| wants.contains(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);
}
}
}
}
let mut pack_oids: HashSet<ObjectId> = HashSet::new();
let mut transfer = TransferStats::default();
let mut shallow_new: Vec<ObjectId> = Vec::new();
let mut shallow_unshallow: Vec<ObjectId> = Vec::new();
let mut copy_source_shallow = remote_shallow.clone();
if opts.has_deepen_request() {
let remote_repo =
crate::repo::Repository::open(remote_git_dir, &RepositoryOptions::empty())?;
let (wire_depth, deepen_relative) = opts
.wire_deepen()
.map(|(d, r)| (Some(d), r))
.unwrap_or((None, false));
let req = crate::serve::ShallowRequest {
client_shallow: local_shallow.iter().copied().collect(),
depth: wire_depth,
deepen_since: opts
.deepen_since
.as_deref()
.and_then(|s| s.parse::<i64>().ok()),
deepen_not: Vec::new(),
deepen_relative,
};
let resp = crate::serve::compute_shallow_response(&remote_repo, &shallow_wants, &req)?;
shallow_new = resp.shallow;
shallow_unshallow = resp.unshallow;
copy_source_shallow = resp.pack_shallow_grafts;
for oid in resp.extra_wants.iter().chain(shallow_new.iter()).copied() {
if !wants.contains(&oid) {
wants.push(oid);
}
}
if opts.tags == TagMode::Following {
let tag_depth = opts.absolute_depth_for_tags().or_else(|| {
opts.wire_deepen().and_then(|(d, relative)| {
if !relative {
return Some(d);
}
let deepen = usize::try_from(d).ok()?;
crate::serve::relative_deepen_depth_for_request(
&remote_repo,
&shallow_wants,
&req.client_shallow,
deepen,
)
.and_then(|d| u32::try_from(d).ok())
})
});
if let Some(depth) =
tag_depth.filter(|d| *d > 0 && *d != crate::shallow::INFINITE_DEPTH)
{
let mut effective_shallow = tag_shallow.clone();
effective_shallow.extend(shallow_new.iter().copied());
for oid in &shallow_unshallow {
effective_shallow.remove(oid);
}
let fetch_commits = crate::fetch::shallow_fetched_commit_oids(
&remote_odb,
&fetch_commit_oids.iter().copied().collect::<Vec<_>>(),
depth,
&effective_shallow,
);
for m in &matched {
if !m.is_tag || local_odb.exists(&m.oid) || wants.contains(&m.oid) {
continue;
}
let peel = remote_odb
.peel_tags(m.oid)
.ok()
.map(|(p, _)| p)
.unwrap_or(m.oid);
if fetch_commits.contains(&peel) {
wants.push(m.oid);
}
}
}
}
for oid in &fetch_commit_oids {
if !wants.contains(oid) {
wants.push(*oid);
}
}
}
if !wants.is_empty() && !opts.dry_run {
let remote_cfg = config_for_git_dir(remote_git_dir);
let pack_opts = PackBuildOptions::for_local_copy(remote_cfg.as_ref());
let (oids, fetch_transfer) = copy_objects_for_local_fetch(
remote_git_dir,
local_git_dir,
&remote_odb,
&local_odb,
&wants,
&haves,
©_source_shallow,
&local_shallow,
!opts.has_deepen_request(),
&pack_opts,
opts,
progress,
)?;
pack_oids = oids;
transfer = fetch_transfer;
}
if opts.initial_remote_fetch
&& !remote_shallow.is_empty()
&& !opts.has_deepen_request()
&& !opts.dry_run
{
let remote_shallow_vec: Vec<ObjectId> = remote_shallow.iter().copied().collect();
crate::shallow::apply_shallow_updates(local_git_dir, &remote_shallow_vec, &[])?;
}
if (!shallow_new.is_empty() || !shallow_unshallow.is_empty()) && !opts.dry_run {
crate::shallow::apply_shallow_updates(local_git_dir, &shallow_new, &shallow_unshallow)?;
}
if opts.tags == TagMode::Following {
let boundaries = crate::shallow::load_shallow_boundaries(local_git_dir);
crate::fetch::retain_following_tags(&local_odb, &mut matched, &pack_oids, &boundaries)?;
}
let classify_repo = if opts.dry_run {
crate::repo::Repository::open(remote_git_dir, &RepositoryOptions::empty()).ok()
} else {
crate::repo::Repository::open(local_git_dir, &RepositoryOptions::empty()).ok()
};
let mut updates: Vec<RefUpdate> = Vec::new();
let mut prune_batch: Vec<crate::refs::store::RefUpdate> = Vec::new();
if opts.prune {
prune_tracking_refs(
local_git_dir,
&positive,
&remote_refs,
opts.dry_run,
&mut updates,
if opts.dry_run {
None
} else {
Some(&mut prune_batch)
},
)?;
}
if !opts.dry_run && !prune_batch.is_empty() {
crate::refs::commit_ref_store_batch(local_git_dir, &prune_batch)?;
}
let packed = crate::refs::PackedRefs::load(local_git_dir)?;
let mut store_batch: Vec<crate::refs::store::RefUpdate> = Vec::new();
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_cached(local_git_dir, local_ref, &packed)
.ok()
.flatten();
let mode = classify_update(
old.as_ref(),
&m.oid,
m.force,
m.dest_is_tag,
classify_repo.as_ref(),
None,
);
let write = matches!(
mode,
UpdateMode::New | UpdateMode::FastForward | UpdateMode::Forced
);
enforce_fetch_ref_update(
local_ref,
old,
m.oid,
mode,
opts,
explicit_refspec_fetch,
classify_repo.as_ref(),
)?;
if write && !opts.dry_run {
if !crate::refs::is_valid_storable_ref_name(local_ref) {
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: Some("skipped (invalid ref name)".to_owned()),
});
continue;
}
if m.dest_is_tag && !ref_target_exists(&local_odb, &remote_odb, 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: UpdateMode::NoChangeNeeded,
note: Some("skipped (tag target not present locally)".to_owned()),
});
continue;
}
store_batch.push(crate::refs::store::RefUpdate {
name: local_ref.clone(),
new_value: Some(crate::refs::store::RawRef::Direct(m.oid)),
expected: crate::refs::store::Expected::Any,
reflog: None,
flags: Default::default(),
});
}
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,
});
}
if let Some(remote) = opts.remote_name.as_deref() {
if !opts.dry_run {
if let Some(branch) = default_branch.as_deref() {
let head_ref = format!("refs/remotes/{remote}/HEAD");
if crate::refs::resolve_ref(local_git_dir, &head_ref).is_err() {
let target = format!("refs/remotes/{remote}/{branch}");
let target_will_exist = store_batch.iter().any(|u| u.name == target)
|| crate::refs::resolve_ref(local_git_dir, &target).is_ok();
if target_will_exist {
store_batch.push(crate::refs::store::RefUpdate {
name: head_ref,
new_value: Some(crate::refs::store::RawRef::Symbolic(target)),
expected: crate::refs::store::Expected::Any,
reflog: None,
flags: Default::default(),
});
}
}
}
}
}
if !opts.dry_run && !store_batch.is_empty() {
crate::refs::commit_ref_store_batch(local_git_dir, &store_batch)?;
}
crate::fetch::finish_initial_remote_fetch_layout(
local_git_dir,
opts,
default_branch.as_deref(),
)?;
if let Some(remote) = opts.remote_name.as_deref() {
crate::fetch::ensure_remote_tracking_head_symref(
local_git_dir,
remote,
default_branch.as_deref(),
opts.dry_run,
)?;
}
finish_anonymous_url_fetch(
local_git_dir,
opts,
explicit_refspec_fetch,
default_branch.as_deref(),
&remote_refs,
&matched,
)?;
let new_shallow = crate::shallow::dedupe_shallow_list(if opts.has_deepen_request() {
shallow_new
} else if opts.initial_remote_fetch {
remote_shallow.iter().copied().collect()
} else {
Vec::new()
});
Ok(FetchOutcome {
updates,
default_branch,
new_shallow,
new_unshallow: shallow_unshallow,
transfer,
})
}
pub fn push_local(
local_git_dir: &Path,
remote_git_dir: &Path,
refs: &[PushRefSpec],
opts: &PushOptions,
) -> Result<PushOutcome> {
if !opts.push_options.is_empty() {
return Err(Error::PushOptionsUnsupported);
}
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 local_repo = crate::repo::Repository::open(local_git_dir, &RepositoryOptions::empty()).ok();
let remote_have_tips: Vec<ObjectId> = match &opts.remote_ref_tips {
Some(tips) => tips.clone(),
None => 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(),
});
}
let push_wants: Vec<ObjectId> = decisions
.iter()
.filter(|d| d.apply && !opts.dry_run)
.filter_map(|d| match &d.action {
PushAction::Update(src) => Some(*src),
_ => None,
})
.collect();
if !push_wants.is_empty() && !opts.dry_run {
let local_cfg = config_for_git_dir(local_git_dir);
let pack_opts = if remote_have_tips.is_empty() {
PackBuildOptions::for_local_copy(local_cfg.as_ref())
} else {
PackBuildOptions::for_local_push(local_cfg.as_ref())
};
let empty_shallow = HashSet::new();
let remote_shallow = crate::shallow::load_shallow_boundaries(remote_git_dir);
copy_objects_for_local_push(
local_git_dir,
remote_git_dir,
&local_odb,
&remote_odb,
&push_wants,
&remote_have_tips,
&empty_shallow,
&remote_shallow,
&pack_opts,
)?;
}
let mut ref_batch: Vec<crate::refs::store::RefUpdate> = Vec::new();
for d in &mut decisions {
if !d.apply || opts.dry_run {
continue;
}
match &d.action {
PushAction::Update(src) => {
let update = push_store_ref_update(
d.result.remote_ref.as_str(),
Some(*src),
d.result.old_oid,
);
if opts.atomic {
ref_batch.push(update);
} else if let Err(err) = crate::refs::commit_store_update(remote_git_dir, update) {
apply_ref_store_error_to_push_decision(d, &err, false);
}
}
PushAction::Delete => {
let update =
push_store_ref_update(d.result.remote_ref.as_str(), None, d.result.old_oid);
if opts.atomic {
ref_batch.push(update);
} else if let Err(err) = crate::refs::commit_store_update(remote_git_dir, update) {
apply_ref_store_error_to_push_decision(d, &err, false);
}
}
PushAction::None => {}
}
}
if opts.atomic && !opts.dry_run && !ref_batch.is_empty() {
if let Err(err) = crate::refs::commit_ref_store_batch(remote_git_dir, &ref_batch) {
apply_atomic_batch_ref_store_error(&mut decisions, &ref_batch, err);
}
}
let results: Vec<_> = decisions.into_iter().map(|d| d.result).collect();
if !opts.dry_run {
if let Some(remote) = opts.tracking_remote.as_deref() {
crate::branch_tracking::apply_push_remote_tracking_updates(
local_git_dir,
remote,
&results,
)?;
}
}
Ok(PushOutcome { results })
}
fn push_store_ref_update(
refname: &str,
new_oid: Option<ObjectId>,
expected_old: Option<ObjectId>,
) -> crate::refs::store::RefUpdate {
crate::refs::store::RefUpdate {
name: refname.to_owned(),
new_value: new_oid.map(crate::refs::store::RawRef::Direct),
expected: match expected_old {
Some(oid) => crate::refs::store::Expected::Oid(oid),
None => crate::refs::store::Expected::Any,
},
reflog: None,
flags: Default::default(),
}
}
fn push_status_for_ref_store_error(err: &Error) -> PushRefStatus {
match err {
Error::RefStore(crate::refs::store::RefStoreError::ExpectedMismatch { .. }) => {
PushRefStatus::RejectStale
}
_ => PushRefStatus::RemoteRejected,
}
}
fn ref_store_error_refname(err: &Error) -> Option<String> {
let Error::RefStore(store_err) = err else {
return None;
};
Some(match store_err {
crate::refs::store::RefStoreError::ExpectedMismatch { name, .. }
| crate::refs::store::RefStoreError::DuplicateUpdate { name }
| crate::refs::store::RefStoreError::LockHeld { name }
| crate::refs::store::RefStoreError::InvalidRefName { name }
| crate::refs::store::RefStoreError::NotFound { name } => name.clone(),
crate::refs::store::RefStoreError::NameUnavailable { reason } => match reason {
crate::refs::RefnameUnavailable::SameBatch { refname, .. }
| crate::refs::RefnameUnavailable::AncestorExists {
new_ref: refname, ..
}
| crate::refs::RefnameUnavailable::DescendantExists {
new_ref: refname, ..
} => refname.clone(),
},
crate::refs::store::RefStoreError::SymrefLoop
| crate::refs::store::RefStoreError::Corrupt(_) => return None,
})
}
fn apply_ref_store_error_to_push_decision(d: &mut PushDecision, err: &Error, _atomic: bool) {
d.result.status = push_status_for_ref_store_error(err);
d.result.message = Some(err.to_string());
d.apply = false;
}
fn apply_atomic_batch_ref_store_error(
decisions: &mut [PushDecision],
batch: &[crate::refs::store::RefUpdate],
err: Error,
) {
let fail_ref = ref_store_error_refname(&err);
let message = err.to_string();
let batch_names: HashSet<&str> = batch.iter().map(|u| u.name.as_str()).collect();
let in_batch = decisions
.iter_mut()
.filter(|d| d.apply && batch_names.contains(d.result.remote_ref.as_str()));
if let Some(fail_name) = fail_ref {
for d in in_batch {
if d.result.remote_ref == fail_name {
d.result.status = push_status_for_ref_store_error(&err);
d.result.message = Some(message.clone());
} else {
d.result.status = PushRefStatus::AtomicPushFailed;
d.result.message = Some("atomic push failed".to_owned());
}
d.apply = false;
}
} else {
let mut first = true;
for d in in_batch {
if first {
d.result.status = push_status_for_ref_store_error(&err);
d.result.message = Some(message.clone());
first = false;
} else {
d.result.status = PushRefStatus::AtomicPushFailed;
d.result.message = Some("atomic push failed".to_owned());
}
d.apply = false;
}
}
}
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: push_spec_local_ref(spec),
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: push_spec_local_ref(spec),
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: push_spec_local_ref(spec),
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: push_spec_local_ref(spec),
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: push_spec_local_ref(spec),
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: push_spec_local_ref(spec),
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: push_spec_local_ref(spec),
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: push_spec_local_ref(spec),
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) dest_is_tag: bool,
pub(crate) advertised_peel: Option<ObjectId>,
pub(crate) fetch_head: crate::fetch_request::FetchHeadPolicy,
}
pub(crate) fn open_odb(git_dir: &Path) -> Odb {
Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.to_path_buf())
}
fn config_for_git_dir(git_dir: &Path) -> Option<crate::config::ConfigSet> {
crate::config::ConfigSet::load(
&crate::environment::Environment::empty(),
Some(git_dir),
true,
)
.ok()
}
pub(crate) fn finish_anonymous_url_fetch(
git_dir: &Path,
opts: &FetchOptions,
explicit_refspec_fetch: bool,
default_branch: Option<&str>,
remote_refs: &[(String, ObjectId)],
matched: &[MatchedRef],
) -> Result<()> {
if opts.dry_run {
return Ok(());
}
let write_fetch_head = opts.remote_name.is_none() || explicit_refspec_fetch;
if !write_fetch_head {
return Ok(());
}
let source = opts.fetch_source.clone().or_else(|| {
let name = opts.remote_name.as_deref()?;
let config = config_for_git_dir(git_dir)?;
config
.get(&format!("remote.{name}.url"))
.filter(|url| !url.trim().is_empty())
});
let Some(source) = source.as_deref() else {
return Ok(());
};
let lines = fetch_head_lines_for_ad_hoc_fetch(
matched,
remote_refs,
&opts.refspecs,
default_branch,
source,
);
if lines.is_empty() {
return Ok(());
}
crate::fetch_head::write_fetch_head(git_dir, &lines)
}
fn fetch_head_lines_for_ad_hoc_fetch(
matched: &[MatchedRef],
remote_refs: &[(String, ObjectId)],
refspecs: &[String],
default_branch: Option<&str>,
source: &str,
) -> Vec<crate::fetch_head::FetchHeadLine> {
use crate::fetch_head::FetchHeadLine;
use crate::remote::Remote;
let anonymous_only = refspecs.len() == 1
&& refspecs[0].trim_start_matches('+') == Remote::ANONYMOUS_FETCH_REFSPEC;
if anonymous_only {
if let Some(branch) = default_branch {
let head_ref = format!("refs/heads/{branch}");
if let Some(m) = matched
.iter()
.find(|m| m.remote_ref == head_ref && !m.is_tag)
{
return vec![FetchHeadLine {
oid: m.oid,
for_merge: true,
description: source.to_owned(),
}];
}
}
if let Some((_, oid)) = remote_refs.iter().find(|(name, _)| name == "HEAD") {
return vec![FetchHeadLine {
oid: *oid,
for_merge: true,
description: source.to_owned(),
}];
}
return Vec::new();
}
matched
.iter()
.filter(|m| m.fetch_head == crate::fetch_request::FetchHeadPolicy::ForMerge)
.map(|m| FetchHeadLine {
oid: m.oid,
for_merge: true,
description: crate::fetch_request::fetch_head_description(
&m.remote_ref,
m.is_tag,
source,
),
})
.collect()
}
#[must_use]
pub(crate) fn remote_head_oid_from_advertised(
advertised: &[(String, ObjectId)],
) -> Option<ObjectId> {
advertised
.iter()
.find(|(name, _)| name == "HEAD")
.map(|(_, oid)| *oid)
}
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))
})
}
pub(crate) 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,
shallow_boundaries: &HashSet<ObjectId>,
negatives: &[RefspecItem],
already_local: &HashSet<String>,
matched: &mut Vec<MatchedRef>,
matched_oids: &mut HashSet<ObjectId>,
seen_remote_ref: &mut HashSet<String>,
) -> Result<HashSet<ObjectId>> {
let mut following_only = HashSet::new();
if mode == TagMode::None {
return Ok(following_only);
}
let candidates: Vec<(&String, ObjectId, Option<ObjectId>)> = remote_refs
.iter()
.filter(|(name, _)| {
name.starts_with("refs/tags/")
&& !seen_remote_ref.contains(name)
&& !ref_excluded(name, negatives)
&& !(mode == TagMode::Following && already_local.contains(name))
})
.map(|(name, oid)| {
let peeled = match mode {
TagMode::Following => remote_odb.peel_tags(*oid).ok().map(|(p, _)| p),
_ => None,
};
(name, *oid, peeled)
})
.collect();
let following_closure: HashSet<ObjectId> = if mode == TagMode::Following {
let roots: Vec<ObjectId> = matched
.iter()
.filter(|m| !m.is_tag)
.map(|m| m.oid)
.collect();
let targets: HashSet<ObjectId> = candidates
.iter()
.map(|(_, oid, peeled)| peeled.unwrap_or(*oid))
.collect();
crate::fetch::reachable_commits(remote_odb, &roots, &targets, shallow_boundaries)
} else {
HashSet::new()
};
for (name, oid, peeled) in candidates {
let keep = match mode {
TagMode::All => true,
TagMode::Following => {
following_closure.contains(&oid)
|| peeled.is_some_and(|p| following_closure.contains(&p))
}
TagMode::None => false,
};
if keep && seen_remote_ref.insert(name.clone()) {
if matched_oids.insert(oid) && mode == TagMode::Following {
following_only.insert(oid);
}
matched.push(MatchedRef {
remote_ref: name.clone(),
local_ref: Some(name.clone()),
oid,
force: false,
is_tag: true,
dest_is_tag: true,
advertised_peel: peeled,
fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
});
}
}
Ok(following_only)
}
fn ref_target_exists(local_odb: &Odb, remote_odb: &Odb, tag_oid: ObjectId) -> bool {
let peel_odb = if local_odb.exists(&tag_oid) {
local_odb
} else {
remote_odb
};
peel_odb
.peel_tags(tag_oid)
.is_ok_and(|(peeled, _)| local_odb.exists(&peeled))
}
pub(crate) fn classify_update(
old: Option<&ObjectId>,
new: &ObjectId,
force: bool,
is_tag: bool,
repo: Option<&crate::repo::Repository>,
classify_odb: Option<&crate::odb::Odb>,
) -> UpdateMode {
let Some(old) = old else {
return UpdateMode::New;
};
if old == new {
return UpdateMode::UpToDate;
}
let ff = if let Some(r) = repo {
crate::merge_base::is_ancestor(r, *old, *new).unwrap_or(false)
} else if let Some(odb) = classify_odb {
crate::merge_base::is_ancestor_odb_walk(odb, *old, *new).unwrap_or(false)
} else {
false
};
if ff && !is_tag {
return UpdateMode::FastForward;
}
if force {
return UpdateMode::Forced;
}
if is_tag {
return UpdateMode::TagUpdateRejected;
}
UpdateMode::NonFastForwardRejected
}
fn is_default_head_under_tracking_prefix(refname: &str, tracking_prefix: &str) -> bool {
refname == format!("{tracking_prefix}HEAD")
}
pub fn tracking_refs_gone_on_remote(
local_git_dir: &Path,
positive: &[RefspecItem],
remote_refs: &[(String, ObjectId)],
) -> Result<Vec<(String, ObjectId)>> {
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 is_default_head_under_tracking_prefix(&name, prefix) {
continue;
}
if !live.contains(&name) {
pruned.entry(name).or_insert(oid);
}
}
} else if !live.contains(dst) {
let exact_prefix = dst.strip_suffix("HEAD").filter(|p| p.ends_with('/'));
if exact_prefix.is_some_and(|p| is_default_head_under_tracking_prefix(dst, p)) {
continue;
}
if let Ok(oid) = crate::refs::resolve_ref(local_git_dir, dst) {
pruned.entry(dst.to_owned()).or_insert(oid);
}
}
}
Ok(pruned.into_iter().collect())
}
pub(crate) fn prune_tracking_refs(
local_git_dir: &Path,
positive: &[RefspecItem],
remote_refs: &[(String, ObjectId)],
dry_run: bool,
updates: &mut Vec<RefUpdate>,
mut store_batch: Option<&mut Vec<crate::refs::store::RefUpdate>>,
) -> Result<()> {
let pruned: Vec<(String, ObjectId)> =
tracking_refs_gone_on_remote(local_git_dir, positive, remote_refs)?;
let prune_delete_flags = crate::refs::store::RefUpdateFlags {
no_deref: true,
..Default::default()
};
for (name, oid) in pruned.into_iter() {
if !dry_run {
if let Some(batch) = store_batch.as_mut() {
batch.push(crate::refs::store::RefUpdate {
name: name.clone(),
new_value: None,
expected: crate::refs::store::Expected::Any,
reflog: None,
flags: prune_delete_flags,
});
} else {
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(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::progress::ProgressPhase;
use crate::ref_storage::RefStorageFormat;
use crate::repo::init_repository;
use std::process::Command;
#[derive(Debug, Default)]
struct PhaseRecorder {
events: Vec<&'static str>,
started: Vec<(ProgressPhase, Option<u64>)>,
finished: Vec<ProgressPhase>,
counts: Vec<u64>,
active: Option<ProgressPhase>,
current: u64,
}
impl ProgressSink for PhaseRecorder {
fn start(&mut self, phase: ProgressPhase, total: Option<u64>) {
self.events.push("start");
self.started.push((phase, total));
self.active = Some(phase);
self.current = 0;
}
fn inc(&mut self, units: u64) {
self.events.push("inc");
self.current += units;
self.counts.push(self.current);
}
fn set(&mut self, current: u64) {
self.events.push("set");
self.current = current;
self.counts.push(self.current);
}
fn finish(&mut self) {
self.events.push("finish");
if let Some(phase) = self.active.take() {
self.finished.push(phase);
}
}
}
impl Progress for PhaseRecorder {}
fn git_ok(dir: &Path, args: &[&str]) -> bool {
Command::new("git") .env("GIT_AUTHOR_NAME", "Test")
.env("GIT_AUTHOR_EMAIL", "test@example.com")
.env("GIT_COMMITTER_NAME", "Test")
.env("GIT_COMMITTER_EMAIL", "test@example.com")
.current_dir(dir)
.args(args)
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn assert_monotonic(counts: &[u64]) {
for pair in counts.windows(2) {
assert!(pair[1] >= pair[0], "progress counts must be monotonic");
}
}
#[test]
fn fetch_local_reports_copy_progress() {
let remote = tempfile::tempdir().expect("remote");
assert!(git_ok(
remote.path(),
&["init", "-q", "-b", "main", "--bare"]
));
let src = tempfile::tempdir().expect("src");
assert!(git_ok(src.path(), &["init", "-q", "-b", "main"]));
std::fs::write(src.path().join("f.txt"), "payload\n").unwrap();
assert!(git_ok(src.path(), &["add", "f.txt"]));
assert!(git_ok(src.path(), &["commit", "-qm", "c1"]));
assert!(git_ok(
src.path(),
&["push", "-q", remote.path().to_str().unwrap(), "main:main",],
));
let local = tempfile::tempdir().expect("local");
init_repository(local.path(), false, "main", None, RefStorageFormat::Files).expect("init");
let mut opts = FetchOptions {
refspecs: vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()],
initial_remote_fetch: true,
tags: TagMode::None,
..Default::default()
};
let mut rec = PhaseRecorder::default();
fetch_local_with_progress(&local.path().join(".git"), remote.path(), &opts, &mut rec)
.expect("fetch_local");
assert!(
rec.started
.iter()
.any(|(p, _)| *p == ProgressPhase::CopyingObjects),
"expected CopyingObjects phase"
);
assert!(
rec.finished.contains(&ProgressPhase::CopyingObjects),
"expected CopyingObjects finish"
);
assert!(!rec.counts.is_empty(), "expected copy progress counts");
assert_monotonic(&rec.counts);
let first_progress = rec.events.iter().find(|e| **e != "start").copied();
assert_eq!(
rec.events.first().copied(),
Some("start"),
"CopyingObjects must start before link/copy work reports progress"
);
assert!(
first_progress.is_some(),
"expected set/inc after start during copy"
);
}
#[test]
fn transfer_stats_for_local_operation_counts_new_pack_index() {
let tmp = tempfile::tempdir().expect("tempdir");
let repo = tmp.path().join("repo");
std::fs::create_dir_all(&repo).unwrap();
assert!(git_ok(&repo, &["init", "-q", "-b", "main", "."]));
std::fs::write(repo.join("blob.txt"), b"payload\n").unwrap();
assert!(git_ok(&repo, &["add", "blob.txt"]));
assert!(git_ok(&repo, &["commit", "-qm", "c"]));
assert!(git_ok(&repo, &["repack", "-a", "-d", "-f"]));
let git_dir = repo.join(".git");
let pack_dir = git_dir.join("objects/pack");
let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.clone());
let stats =
transfer_stats_for_local_operation(&git_dir, &odb, &HashSet::new(), &HashSet::new())
.expect("stats");
assert!(stats.bytes > 0);
assert!(stats.objects > 0);
let pack_path = std::fs::read_dir(&pack_dir)
.expect("read pack dir")
.filter_map(|e| e.ok())
.map(|e| e.path())
.find(|p| p.extension().is_some_and(|e| e == "pack"))
.expect("pack file");
let idx = pack_path.with_extension("idx");
let index = crate::pack::read_pack_index_no_verify(&idx).expect("idx");
assert_eq!(
stats.objects,
u64::try_from(index.len()).expect("count"),
"object count must match the on-disk index entry count"
);
}
#[test]
fn update_mode_modified_repo_excludes_rejected_tag_updates() {
assert!(UpdateMode::New.modified_repo());
assert!(UpdateMode::DeletedMissing.modified_repo());
assert!(!UpdateMode::TagUpdateRejected.modified_repo());
assert!(!UpdateMode::UpToDate.modified_repo());
}
#[test]
fn update_mode_report_classification_covers_every_mode() {
use UpdateMode::*;
for (mode, reported, rejected) in [
(New, true, false),
(FastForward, true, false),
(Forced, true, false),
(DeletedMissing, true, false),
(NonFastForwardRejected, true, true),
(TagUpdateRejected, true, true),
(SourceObjectNotFound, true, true),
(UpToDate, false, false),
(NoChangeNeeded, false, false),
(Unborn, false, false),
] {
assert_eq!(mode.is_reported(), reported, "{mode:?}");
assert_eq!(mode.is_rejected(), rejected, "{mode:?}");
assert!(!(mode.modified_repo() && mode.is_rejected()), "{mode:?}");
}
}
#[test]
fn fetch_outcome_summarizes_reported_changed_and_rejected_refs() {
let update = |name: &str, mode| RefUpdate {
remote_ref: format!("refs/heads/{name}"),
local_ref: Some(format!("refs/remotes/origin/{name}")),
old_oid: None,
new_oid: None,
mode,
note: None,
};
let outcome = FetchOutcome {
updates: vec![
update("same", UpdateMode::UpToDate),
update("new", UpdateMode::New),
update("ff", UpdateMode::FastForward),
update("noop", UpdateMode::NoChangeNeeded),
],
..Default::default()
};
let reported: Vec<&str> = outcome
.reported_updates()
.map(|u| u.remote_ref.as_str())
.collect();
assert_eq!(reported, ["refs/heads/new", "refs/heads/ff"]);
assert_eq!(outcome.changed_ref_count(), 2);
assert!(!outcome.has_rejections());
let rejected = FetchOutcome {
updates: vec![update("diverged", UpdateMode::NonFastForwardRejected)],
..Default::default()
};
assert_eq!(rejected.changed_ref_count(), 0);
assert!(rejected.has_rejections());
assert_eq!(rejected.reported_updates().count(), 1);
assert!(FetchOutcome::default().reported_updates().next().is_none());
}
}