#![allow(clippy::result_large_err)]use std::{collections::BTreeMap, path::PathBuf}; use gix_object::Exists;use gix_ref::{ Target, TargetRef, transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog},}; use crate::{ Repository, ext::ObjectIdExt, remote::{ fetch, fetch::{ RefLogMessage, refmap::Source, refs::update::{Mode, TypeChange}, }, },}; ///pub mod update; /// Information about the update of a single reference, corresponding the respective entry in [`RefMap::mappings`][crate::remote::fetch::RefMap::mappings].#[derive(Debug, Clone, PartialEq, Eq)]pub struct Update { /// The way the update was performed. pub mode: Mode, /// If not `None`, the update also affects the type of the reference. This also implies that `edit_index` is not None. pub type_change: Option<TypeChange>, /// The index to the edit that was created from the corresponding mapping, or `None` if there was no local ref. pub edit_index: Option<usize>,} impl From<Mode> for Update { fn from(mode: Mode) -> Self { Update { mode, type_change: None, edit_index: None, } }} /// Update all refs as derived from `refmap.mappings` and produce an `Outcome` informing about all applied changes in detail, with each/// [`update`][Update] corresponding to the [`fetch::Mapping`] of at the same index./// If `dry_run` is true, ref transactions won't actually be applied, but are assumed to work without error so the underlying/// `repo` is not actually changed. Also it won't perform an 'object exists' check as these are likely not to exist as the pack/// wasn't fetched either./// `action` is the prefix used for reflog entries, and is typically "fetch".////// It can be used to produce typical information that one is used to from `git fetch`.////// We will reject updates only if…////// * …fast-forward rules are violated/// * …the local ref is currently checked out/// * …existing refs would not become 'unborn', i.e. point to a reference that doesn't exist and won't be created due to ref-specs////// With these safeguards in place, one can handle each naturally and implement mirrors or bare repos easily.#[expect(clippy::too_many_arguments)]pub(crate) fn update( repo: &Repository, message: RefLogMessage, mappings: &[fetch::refmap::Mapping], refspecs: &[gix_refspec::RefSpec], extra_refspecs: &[gix_refspec::RefSpec], fetch_tags: fetch::Tags, dry_run: fetch::DryRun, write_packed_refs: fetch::WritePackedRefs,) -> Result<update::Outcome, update::Error> { let _span = gix_trace::detail!("update_refs()", mappings = mappings.len()); let mut edits = Vec::new(); let mut updates = Vec::new(); let mut edit_indices_to_validate = Vec::new(); let mut checked_out_branches = worktree_branches(repo)?; let implicit_tag_refspec = fetch_tags .to_refspec() .filter(|_| matches!(fetch_tags, crate::remote::fetch::Tags::Included)); for (remote, local, spec, is_implicit_tag) in mappings.iter().filter_map( |fetch::refmap::Mapping { remote, local, spec_index, }| { spec_index.get(refspecs, extra_refspecs).map(|spec| { ( remote, local, spec, implicit_tag_refspec.is_some_and(|tag_spec| spec.to_ref() == tag_spec), ) }) }, ) { // `None` only if unborn. let remote_id = remote.as_id(); if matches!(dry_run, fetch::DryRun::No) && !remote_id.is_none_or(|id| repo.objects.exists(id)) { if let Some(remote_id) = remote_id.filter(|id| !repo.objects.exists(id)) { let update = if is_implicit_tag { Mode::ImplicitTagNotSentByRemote.into() } else { // Assure the ODB is not to blame for the missing object. repo.try_find_object(remote_id)?; Mode::RejectedSourceObjectNotFound { id: remote_id.into() }.into() }; updates.push(update); continue; } } let (mode, edit_index, type_change) = match local { Some(name) => { let (mode, reflog_message, name, previous_value) = match repo.try_find_reference(name)? { Some(existing) => { if let Some(wt_dirs) = checked_out_branches.get_mut(existing.name()) { wt_dirs.sort(); wt_dirs.dedup(); let mode = Mode::RejectedCurrentlyCheckedOut { worktree_dirs: wt_dirs.to_owned(), }; updates.push(mode.into()); continue; } match existing .try_id() .map_or_else(|| existing.clone().peel_to_id(), Ok) .map(crate::Id::detach) { Ok(local_id) => { let remote_id = match remote_id { Some(id) => id, None => { // we don't allow to go back to unborn state if there is a local reference already present. // Note that we will be changing it to a symbolic reference just fine. updates.push(Mode::RejectedToReplaceWithUnborn.into()); continue; } }; let (mode, reflog_message) = if local_id == remote_id { (Mode::NoChangeNeeded, "no update will be performed") } else if let Some(gix_ref::Category::Tag) = existing.name().category() { if spec.allow_non_fast_forward() { (Mode::Forced, "updating tag") } else { updates.push(Mode::RejectedTagUpdate.into()); continue; } } else { let mut force = spec.allow_non_fast_forward(); let is_fast_forward = match dry_run { fetch::DryRun::No => { let ancestors = repo .find_object(local_id)? .try_into_commit() .map_err(|_| ()) .and_then(|c| c.committer().map(|a| a.seconds()).map_err(|_| ())) .and_then(|local_commit_time| { remote_id .to_owned() .ancestors(&repo.objects) .sorting( gix_traverse::commit::simple::Sorting::ByCommitTimeCutoff { order: Default::default(), seconds: local_commit_time, }, ) .map_err(|_| ()) }); match ancestors { Ok(mut ancestors) => { ancestors.any(|cid| cid.is_ok_and(|c| c.id == local_id)) } Err(_) => { force = true; false } } } fetch::DryRun::Yes => true, }; if is_fast_forward { ( Mode::FastForward, matches!(dry_run, fetch::DryRun::Yes) .then(|| "fast-forward (guessed in dry-run)") .unwrap_or("fast-forward"), ) } else if force { (Mode::Forced, "forced-update") } else { updates.push(Mode::RejectedNonFastForward.into()); continue; } }; ( mode, reflog_message, existing.name().to_owned(), PreviousValue::MustExistAndMatch(existing.target().into_owned()), ) } Err(crate::reference::peel::Error::ToId(gix_ref::peel::to_id::Error::FollowToObject( gix_ref::peel::to_object::Error::Follow(_), ))) => { // An unborn reference, always allow it to be changed to whatever the remote wants. ( if existing.target().try_name().map(gix_ref::FullNameRef::as_bstr) == remote.as_target() { Mode::NoChangeNeeded } else { Mode::Forced }, "change unborn ref", existing.name().to_owned(), PreviousValue::MustExistAndMatch(existing.target().into_owned()), ) } Err(err) => return Err(err.into()), } } None => { let name: gix_ref::FullName = name.try_into()?; let reflog_msg = match name.category() { Some(gix_ref::Category::Tag) => "storing tag", Some(gix_ref::Category::LocalBranch) => "storing head", _ => "storing ref", }; ( Mode::New, reflog_msg, name, PreviousValue::ExistingMustMatch(new_value_by_remote(remote)?), ) } }; let new = new_value_by_remote(remote)?; let type_change = match (&previous_value, &new) { ( PreviousValue::ExistingMustMatch(Target::Object(_)) | PreviousValue::MustExistAndMatch(Target::Object(_)), Target::Symbolic(_), ) => Some(TypeChange::DirectToSymbolic), ( PreviousValue::ExistingMustMatch(Target::Symbolic(_)) | PreviousValue::MustExistAndMatch(Target::Symbolic(_)), Target::Object(_), ) => Some(TypeChange::SymbolicToDirect), _ => None, }; // We are here because this edit should work and fast-forward rules are respected. // But for setting a symref-target, we have to be sure that the target already exists // or will exists. To be sure all rules are respected, we delay the check to when the // edit-list has been built. let edit_index = edits.len(); if matches!(new, Target::Symbolic(_)) { let anticipated_update_index = updates.len(); edit_indices_to_validate.push((anticipated_update_index, edit_index)); } let edit = RefEdit { change: Change::Update { log: LogChange { mode: RefLog::AndReference, force_create_reflog: false, message: message.compose(reflog_message), }, expected: previous_value, new, }, name, // We must not deref symrefs or we will overwrite their destination, which might be checked out // and we don't check for that case. deref: false, }; edits.push(edit); (mode, Some(edit_index), type_change) } None => (Mode::NoChangeNeeded, None, None), }; updates.push(Update { mode, type_change, edit_index, }); } for (update_index, edit_index) in edit_indices_to_validate { let edit = &edits[edit_index]; if update_needs_adjustment_as_edits_symbolic_target_is_missing(edit, repo, &edits) { let edit = &mut edits[edit_index]; let update = &mut updates[update_index]; update.mode = Mode::RejectedToReplaceWithUnborn; update.type_change = None; match edit.change { Change::Update { ref expected, ref mut new, ref mut log, .. } => match expected { PreviousValue::MustExistAndMatch(existing) => { *new = existing.clone(); log.message = "no-op".into(); } _ => unreachable!("at this point it can only be one variant"), }, Change::Delete { .. } => { unreachable!("we don't do that here") } } } } let edits = match dry_run { fetch::DryRun::No => { let _span = gix_trace::detail!("apply", edits = edits.len()); let (file_lock_fail, packed_refs_lock_fail) = repo .config .lock_timeout() .map_err(crate::reference::edit::Error::from)?; repo.refs .transaction() .packed_refs( match write_packed_refs { fetch::WritePackedRefs::Only => { gix_ref::file::transaction::PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(Box::new(&repo.objects))}, fetch::WritePackedRefs::Never => gix_ref::file::transaction::PackedRefs::DeletionsOnly } ) .prepare(edits, file_lock_fail, packed_refs_lock_fail) .map_err(crate::reference::edit::Error::from)? .commit(repo.committer().transpose().map_err(|err| update::Error::EditReferences(crate::reference::edit::Error::ParseCommitterTime(err)))?) .map_err(crate::reference::edit::Error::from)? } fetch::DryRun::Yes => edits, }; Ok(update::Outcome { edits, updates })} /// Figure out if target of `edit` points to a reference that doesn't exist in `repo` and won't exist as it's not in any of `edits`./// If so, return true.fn update_needs_adjustment_as_edits_symbolic_target_is_missing( edit: &RefEdit, repo: &Repository, edits: &[RefEdit],) -> bool { match edit.change.new_value().expect("here we need a symlink") { TargetRef::Object(_) => unreachable!("BUG: we already know it's symbolic"), TargetRef::Symbolic(new_target_ref) => { match &edit.change { Change::Update { expected, .. } => match expected { PreviousValue::MustExistAndMatch(current_target) => { if let Target::Symbolic(current_target_name) = current_target { if current_target_name.as_ref() == new_target_ref { return false; // no-op are always fine } let current_is_unborn = repo.refs.try_find(current_target_name).ok().flatten().is_none(); if current_is_unborn { return false; } } } PreviousValue::ExistingMustMatch(_) => return false, // this means the ref doesn't exist locally, so we can create unborn refs anyway _ => { unreachable!("BUG: we don't do that here") } }, Change::Delete { .. } => { unreachable!("we don't ever delete here") } } let target_ref_exists_locally = repo.refs.try_find(new_target_ref).ok().flatten().is_some(); if target_ref_exists_locally { return false; } let target_ref_will_be_created = edits.iter().any(|edit| edit.name.as_ref() == new_target_ref); !target_ref_will_be_created } }} /// Convert the remote source into the value to write locally.////// Born symbolic remote refs are written as direct refs to the advertised target object id./// Unborn remote refs remain symbolic as there is no object id to write.fn new_value_by_remote(remote: &Source) -> Result<Target, update::Error> { let remote_id = remote.as_id(); Ok( if let Source::Ref( gix_protocol::handshake::Ref::Symbolic { target, .. } | gix_protocol::handshake::Ref::Unborn { target, .. }, ) = &remote { match remote_id { Some(desired_id) => Target::Object(desired_id.to_owned()), // Unborn branches we create as such, with the location they point to on the remote which helps mirroring. None => Target::Symbolic(target.try_into()?), } } else { Target::Object(remote_id.expect("unborn case handled earlier").to_owned()) }, )} fn insert_head( head: Option<crate::Head<'_>>, out: &mut BTreeMap<gix_ref::FullName, Vec<PathBuf>>,) -> Result<(), update::Error> { if let Some((head, wd)) = head.and_then(|head| head.repo.workdir().map(|wd| (head, wd))) { out.entry("HEAD".try_into().expect("valid")) .or_default() .push(wd.to_owned()); let mut ref_chain = Vec::new(); let mut cursor = head.try_into_referent(); while let Some(ref_) = cursor { ref_chain.push(ref_.name().to_owned()); cursor = ref_.follow().transpose()?; } for name in ref_chain { out.entry(name).or_default().push(wd.to_owned()); } } Ok(())} fn worktree_branches(repo: &Repository) -> Result<BTreeMap<gix_ref::FullName, Vec<PathBuf>>, update::Error> { let mut map = BTreeMap::new(); insert_head(repo.head().ok(), &mut map)?; for proxy in repo.worktrees()? { let repo = proxy.into_repo_with_possibly_inaccessible_worktree()?; insert_head(repo.head().ok(), &mut map)?; } Ok(map)} #[cfg(test)]mod tests;
#![allow(clippy::result_large_err)]
use std::{collections::BTreeMap, path::PathBuf};
use gix_object::Exists;
use gix_ref::{
Target, TargetRef,
transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog},
};
use crate::{
Repository,
ext::ObjectIdExt,
remote::{
fetch,
fetch::{
RefLogMessage,
refmap::Source,
refs::update::{Mode, TypeChange},
},
},
};
///
pub mod update;
/// Information about the update of a single reference, corresponding the respective entry in [`RefMap::mappings`][crate::remote::fetch::RefMap::mappings].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Update {
/// The way the update was performed.
pub mode: Mode,
/// If not `None`, the update also affects the type of the reference. This also implies that `edit_index` is not None.
pub type_change: Option<TypeChange>,
/// The index to the edit that was created from the corresponding mapping, or `None` if there was no local ref.
pub edit_index: Option<usize>,
}
impl From<Mode> for Update {
fn from(mode: Mode) -> Self {
Update {
mode,
type_change: None,
edit_index: None,
}
}
}
/// Update all refs as derived from `refmap.mappings` and produce an `Outcome` informing about all applied changes in detail, with each
/// [`update`][Update] corresponding to the [`fetch::Mapping`] of at the same index.
/// If `dry_run` is true, ref transactions won't actually be applied, but are assumed to work without error so the underlying
/// `repo` is not actually changed. Also it won't perform an 'object exists' check as these are likely not to exist as the pack
/// wasn't fetched either.
/// `action` is the prefix used for reflog entries, and is typically "fetch".
///
/// It can be used to produce typical information that one is used to from `git fetch`.
///
/// We will reject updates only if…
///
/// * …fast-forward rules are violated
/// * …the local ref is currently checked out
/// * …existing refs would not become 'unborn', i.e. point to a reference that doesn't exist and won't be created due to ref-specs
///
/// With these safeguards in place, one can handle each naturally and implement mirrors or bare repos easily.
#[expect(clippy::too_many_arguments)]
pub(crate) fn update(
repo: &Repository,
message: RefLogMessage,
mappings: &[fetch::refmap::Mapping],
refspecs: &[gix_refspec::RefSpec],
extra_refspecs: &[gix_refspec::RefSpec],
fetch_tags: fetch::Tags,
dry_run: fetch::DryRun,
write_packed_refs: fetch::WritePackedRefs,
) -> Result<update::Outcome, update::Error> {
let _span = gix_trace::detail!("update_refs()", mappings = mappings.len());
let mut edits = Vec::new();
let mut updates = Vec::new();
let mut edit_indices_to_validate = Vec::new();
let mut checked_out_branches = worktree_branches(repo)?;
let implicit_tag_refspec = fetch_tags
.to_refspec()
.filter(|_| matches!(fetch_tags, crate::remote::fetch::Tags::Included));
for (remote, local, spec, is_implicit_tag) in mappings.iter().filter_map(
|fetch::refmap::Mapping {
remote,
local,
spec_index,
}| {
spec_index.get(refspecs, extra_refspecs).map(|spec| {
(
remote,
local,
spec,
implicit_tag_refspec.is_some_and(|tag_spec| spec.to_ref() == tag_spec),
)
})
},
) {
// `None` only if unborn.
let remote_id = remote.as_id();
if matches!(dry_run, fetch::DryRun::No) && !remote_id.is_none_or(|id| repo.objects.exists(id)) {
if let Some(remote_id) = remote_id.filter(|id| !repo.objects.exists(id)) {
let update = if is_implicit_tag {
Mode::ImplicitTagNotSentByRemote.into()
} else {
// Assure the ODB is not to blame for the missing object.
repo.try_find_object(remote_id)?;
Mode::RejectedSourceObjectNotFound { id: remote_id.into() }.into()
};
updates.push(update);
continue;
}
}
let (mode, edit_index, type_change) = match local {
Some(name) => {
let (mode, reflog_message, name, previous_value) = match repo.try_find_reference(name)? {
Some(existing) => {
if let Some(wt_dirs) = checked_out_branches.get_mut(existing.name()) {
wt_dirs.sort();
wt_dirs.dedup();
let mode = Mode::RejectedCurrentlyCheckedOut {
worktree_dirs: wt_dirs.to_owned(),
};
updates.push(mode.into());
continue;
}
match existing
.try_id()
.map_or_else(|| existing.clone().peel_to_id(), Ok)
.map(crate::Id::detach)
{
Ok(local_id) => {
let remote_id = match remote_id {
Some(id) => id,
None => {
// we don't allow to go back to unborn state if there is a local reference already present.
// Note that we will be changing it to a symbolic reference just fine.
updates.push(Mode::RejectedToReplaceWithUnborn.into());
continue;
}
};
let (mode, reflog_message) = if local_id == remote_id {
(Mode::NoChangeNeeded, "no update will be performed")
} else if let Some(gix_ref::Category::Tag) = existing.name().category() {
if spec.allow_non_fast_forward() {
(Mode::Forced, "updating tag")
} else {
updates.push(Mode::RejectedTagUpdate.into());
continue;
}
} else {
let mut force = spec.allow_non_fast_forward();
let is_fast_forward = match dry_run {
fetch::DryRun::No => {
let ancestors = repo
.find_object(local_id)?
.try_into_commit()
.map_err(|_| ())
.and_then(|c| c.committer().map(|a| a.seconds()).map_err(|_| ()))
.and_then(|local_commit_time| {
remote_id
.to_owned()
.ancestors(&repo.objects)
.sorting(
gix_traverse::commit::simple::Sorting::ByCommitTimeCutoff {
order: Default::default(),
seconds: local_commit_time,
},
)
.map_err(|_| ())
});
match ancestors {
Ok(mut ancestors) => {
ancestors.any(|cid| cid.is_ok_and(|c| c.id == local_id))
}
Err(_) => {
force = true;
false
}
}
}
fetch::DryRun::Yes => true,
};
if is_fast_forward {
(
Mode::FastForward,
matches!(dry_run, fetch::DryRun::Yes)
.then(|| "fast-forward (guessed in dry-run)")
.unwrap_or("fast-forward"),
)
} else if force {
(Mode::Forced, "forced-update")
} else {
updates.push(Mode::RejectedNonFastForward.into());
continue;
}
};
(
mode,
reflog_message,
existing.name().to_owned(),
PreviousValue::MustExistAndMatch(existing.target().into_owned()),
)
}
Err(crate::reference::peel::Error::ToId(gix_ref::peel::to_id::Error::FollowToObject(
gix_ref::peel::to_object::Error::Follow(_),
))) => {
// An unborn reference, always allow it to be changed to whatever the remote wants.
(
if existing.target().try_name().map(gix_ref::FullNameRef::as_bstr)
== remote.as_target()
{
Mode::NoChangeNeeded
} else {
Mode::Forced
},
"change unborn ref",
existing.name().to_owned(),
PreviousValue::MustExistAndMatch(existing.target().into_owned()),
)
}
Err(err) => return Err(err.into()),
}
}
None => {
let name: gix_ref::FullName = name.try_into()?;
let reflog_msg = match name.category() {
Some(gix_ref::Category::Tag) => "storing tag",
Some(gix_ref::Category::LocalBranch) => "storing head",
_ => "storing ref",
};
(
Mode::New,
reflog_msg,
name,
PreviousValue::ExistingMustMatch(new_value_by_remote(remote)?),
)
}
};
let new = new_value_by_remote(remote)?;
let type_change = match (&previous_value, &new) {
(
PreviousValue::ExistingMustMatch(Target::Object(_))
| PreviousValue::MustExistAndMatch(Target::Object(_)),
Target::Symbolic(_),
) => Some(TypeChange::DirectToSymbolic),
(
PreviousValue::ExistingMustMatch(Target::Symbolic(_))
| PreviousValue::MustExistAndMatch(Target::Symbolic(_)),
Target::Object(_),
) => Some(TypeChange::SymbolicToDirect),
_ => None,
};
// We are here because this edit should work and fast-forward rules are respected.
// But for setting a symref-target, we have to be sure that the target already exists
// or will exists. To be sure all rules are respected, we delay the check to when the
// edit-list has been built.
let edit_index = edits.len();
if matches!(new, Target::Symbolic(_)) {
let anticipated_update_index = updates.len();
edit_indices_to_validate.push((anticipated_update_index, edit_index));
}
let edit = RefEdit {
change: Change::Update {
log: LogChange {
mode: RefLog::AndReference,
force_create_reflog: false,
message: message.compose(reflog_message),
},
expected: previous_value,
new,
},
name,
// We must not deref symrefs or we will overwrite their destination, which might be checked out
// and we don't check for that case.
deref: false,
};
edits.push(edit);
(mode, Some(edit_index), type_change)
}
None => (Mode::NoChangeNeeded, None, None),
};
updates.push(Update {
mode,
type_change,
edit_index,
});
}
for (update_index, edit_index) in edit_indices_to_validate {
let edit = &edits[edit_index];
if update_needs_adjustment_as_edits_symbolic_target_is_missing(edit, repo, &edits) {
let edit = &mut edits[edit_index];
let update = &mut updates[update_index];
update.mode = Mode::RejectedToReplaceWithUnborn;
update.type_change = None;
match edit.change {
Change::Update {
ref expected,
ref mut new,
ref mut log,
..
} => match expected {
PreviousValue::MustExistAndMatch(existing) => {
*new = existing.clone();
log.message = "no-op".into();
}
_ => unreachable!("at this point it can only be one variant"),
},
Change::Delete { .. } => {
unreachable!("we don't do that here")
}
}
}
}
let edits = match dry_run {
fetch::DryRun::No => {
let _span = gix_trace::detail!("apply", edits = edits.len());
let (file_lock_fail, packed_refs_lock_fail) = repo
.config
.lock_timeout()
.map_err(crate::reference::edit::Error::from)?;
repo.refs
.transaction()
.packed_refs(
match write_packed_refs {
fetch::WritePackedRefs::Only => {
gix_ref::file::transaction::PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(Box::new(&repo.objects))},
fetch::WritePackedRefs::Never => gix_ref::file::transaction::PackedRefs::DeletionsOnly
}
)
.prepare(edits, file_lock_fail, packed_refs_lock_fail)
.map_err(crate::reference::edit::Error::from)?
.commit(repo.committer().transpose().map_err(|err| update::Error::EditReferences(crate::reference::edit::Error::ParseCommitterTime(err)))?)
.map_err(crate::reference::edit::Error::from)?
}
fetch::DryRun::Yes => edits,
};
Ok(update::Outcome { edits, updates })
}
/// Figure out if target of `edit` points to a reference that doesn't exist in `repo` and won't exist as it's not in any of `edits`.
/// If so, return true.
fn update_needs_adjustment_as_edits_symbolic_target_is_missing(
edit: &RefEdit,
repo: &Repository,
edits: &[RefEdit],
) -> bool {
match edit.change.new_value().expect("here we need a symlink") {
TargetRef::Object(_) => unreachable!("BUG: we already know it's symbolic"),
TargetRef::Symbolic(new_target_ref) => {
match &edit.change {
Change::Update { expected, .. } => match expected {
PreviousValue::MustExistAndMatch(current_target) => {
if let Target::Symbolic(current_target_name) = current_target {
if current_target_name.as_ref() == new_target_ref {
return false; // no-op are always fine
}
let current_is_unborn = repo.refs.try_find(current_target_name).ok().flatten().is_none();
if current_is_unborn {
return false;
}
}
}
PreviousValue::ExistingMustMatch(_) => return false, // this means the ref doesn't exist locally, so we can create unborn refs anyway
_ => {
unreachable!("BUG: we don't do that here")
}
},
Change::Delete { .. } => {
unreachable!("we don't ever delete here")
}
}
let target_ref_exists_locally = repo.refs.try_find(new_target_ref).ok().flatten().is_some();
if target_ref_exists_locally {
return false;
}
let target_ref_will_be_created = edits.iter().any(|edit| edit.name.as_ref() == new_target_ref);
!target_ref_will_be_created
}
}
}
/// Convert the remote source into the value to write locally.
///
/// Born symbolic remote refs are written as direct refs to the advertised target object id.
/// Unborn remote refs remain symbolic as there is no object id to write.
fn new_value_by_remote(remote: &Source) -> Result<Target, update::Error> {
let remote_id = remote.as_id();
Ok(
if let Source::Ref(
gix_protocol::handshake::Ref::Symbolic { target, .. } | gix_protocol::handshake::Ref::Unborn { target, .. },
) = &remote
{
match remote_id {
Some(desired_id) => Target::Object(desired_id.to_owned()),
// Unborn branches we create as such, with the location they point to on the remote which helps mirroring.
None => Target::Symbolic(target.try_into()?),
}
} else {
Target::Object(remote_id.expect("unborn case handled earlier").to_owned())
},
)
}
fn insert_head(
head: Option<crate::Head<'_>>,
out: &mut BTreeMap<gix_ref::FullName, Vec<PathBuf>>,
) -> Result<(), update::Error> {
if let Some((head, wd)) = head.and_then(|head| head.repo.workdir().map(|wd| (head, wd))) {
out.entry("HEAD".try_into().expect("valid"))
.or_default()
.push(wd.to_owned());
let mut ref_chain = Vec::new();
let mut cursor = head.try_into_referent();
while let Some(ref_) = cursor {
ref_chain.push(ref_.name().to_owned());
cursor = ref_.follow().transpose()?;
}
for name in ref_chain {
out.entry(name).or_default().push(wd.to_owned());
}
}
Ok(())
}
fn worktree_branches(repo: &Repository) -> Result<BTreeMap<gix_ref::FullName, Vec<PathBuf>>, update::Error> {
let mut map = BTreeMap::new();
insert_head(repo.head().ok(), &mut map)?;
for proxy in repo.worktrees()? {
let repo = proxy.into_repo_with_possibly_inaccessible_worktree()?;
insert_head(repo.head().ok(), &mut map)?;
}
Ok(map)
}
#[cfg(test)]
mod tests;