use gix_error::ResultExt;
use gix_object::Exists;
use gix_ref::{
Target, TargetRef,
transaction::{Change, PreviousValue, RefEdit},
};
use crate::{
Repository, Result,
ext::ObjectIdExt,
remote::{
fetch,
fetch::{
RefLogMessage,
refmap::Source,
refs::update::{Mode, TypeChange},
},
},
};
pub mod update;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Update {
pub mode: Mode,
pub type_change: Option<TypeChange>,
pub edit_index: Option<usize>,
}
impl From<Mode> for Update {
fn from(mode: Mode) -> Self {
Update {
mode,
type_change: None,
edit_index: None,
}
}
}
#[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> {
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 = repo.checked_out_branches()?;
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),
)
})
},
) {
let remote_id = remote.as_id();
if matches!(dry_run, fetch::DryRun::No)
&& !remote_id.is_none_or(|id| repo.objects.exists(id))
&& let Some(remote_id) = remote_id.filter(|id| !repo.objects.exists(id))
{
let update = if is_implicit_tag {
Mode::ImplicitTagNotSentByRemote.into()
} else {
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)
.or_raise(|| gix_error::message("Could not peel symbolic local reference to its ID"))
.map(crate::Id::detach)
{
Ok(local_id) => {
let remote_id = match remote_id {
Some(id) => id,
None => {
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 local = repo.find_object(local_id)?;
if local.kind == gix_object::Kind::Commit
&& repo.find_header(remote_id)?.kind() == gix_object::Kind::Commit
{
let local_commit_time = local.into_commit().committer()?.seconds();
let mut ancestors =
remote_id.to_owned().ancestors(&repo.objects).sorting(
gix_traverse::commit::simple::Sorting::ByCommitTimeCutoff {
order: Default::default(),
seconds: local_commit_time,
},
)?;
ancestors
.find(|entry| {
entry.as_ref().map_or(true, |entry| entry.id == local_id)
})
.transpose()?
.is_some()
} else {
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(err) if err.downcast_any_ref::<gix_ref::file::find::NotFound>().is_some() => {
(
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),
}
}
None => {
let name = gix_ref::FullName::try_from(name).or_raise(|| {
gix_error::message(
"A remote reference had a name that wasn't considered valid. \
Corrupt remote repo or insufficient checks on remote?",
)
})?;
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,
};
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::update(name, new, previous_value, message.compose(reflog_message));
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().or_raise(|| {
gix_error::message("Failed to update references to their new position to match their remote locations")
})?;
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)
.or_raise(|| {
gix_error::message(
"Failed to update references to their new position to match their remote locations",
)
})?
.commit(
repo.committer()
.transpose()
.or_raise(|| {
gix_error::message(
"Failed to update references to their new position to match their remote locations",
)
})?,
)
.or_raise(|| {
gix_error::message(
"Failed to update references to their new position to match their remote locations",
)
})?
}
fetch::DryRun::Yes => edits,
};
Ok(update::Outcome { edits, updates })
}
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; }
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, _ => {
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
}
}
}
fn new_value_by_remote(remote: &Source) -> Result<Target> {
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()),
None => Target::Symbolic(gix_ref::FullName::try_from(target).or_raise(|| {
gix_error::message(
"A remote reference had a name that wasn't considered valid. \
Corrupt remote repo or insufficient checks on remote?",
)
})?),
}
} else {
Target::Object(remote_id.expect("unborn case handled earlier").to_owned())
},
)
}
#[cfg(test)]
mod tests;