GitoxideLabs/gitoxide · error

A reference named ' ' has multiple edits

Error message

A reference named '{name}' has multiple edits

What it means

During reference transaction pre-processing, gix-ref checks that each reference name appears in at most one edit; if `assure_one_name_has_one_edit` finds duplicates, pre_process returns an io::Error of kind AlreadyExists: "A reference named '{name}' has multiple edits". Git semantics forbid two conflicting updates to the same ref within one transaction.

Solutions

  1. Deduplicate RefEdits by reference name before preparing the transaction (later edit wins)
  2. Split conflicting operations into separate sequential transactions
  3. Audit code paths that build edit lists to ensure they don't both touch the same ref
  4. If intentional replace is needed, remove the earlier edit rather than appending a second one

Example fix

// before
let tx = repo.edit_reference(edits::Reference::Create(name, target1, msg));
// ... elsewhere
transaction.prepare(edits2) // edits2 contains a second edit for `name`

// after
let mut edits: Vec<RefEdit> = edits1.into_iter()
    .chain(edits2)
    .fold(BTreeMap::new(), |mut m, e| { m.insert(e.name.clone(), e); m })
    .into_values().collect();
Defensive patterns

Strategy: validation

Validate before calling

// before preparing the transaction
echo "checking for duplicate ref edits" >&2;
let mut names = std::collections::HashSet::new();
for edit in &edits {
    if !names.insert(edit.name.as_bstr().to_vec()) {
        return Err(format!("duplicate edit for {}", edit.name));
    }
}

Prevention

When it happens

Trigger: Calling `gix_ref::transaction::Transaction::pre_process` (with a `find` lookup and `make_entry` factory) after adding two RefEdits for the same reference name into the same transaction.

Common situations: Application logic queuing e.g. both a create and an update for HEAD or the same branch; concurrent code paths each adding an edit for the same ref; migration scripts replaying edits twice.

Understand the failure class

Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.

Related errors


AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08). Data as JSON: /api/errors/30fab49f0fd19841. Report an issue: GitHub.

Appendix: source

Thrown at gix-ref/src/transaction/ext.rs:35

    ///
    /// Note no action is performed if deref isn't specified.
    fn extend_with_splits_of_symbolic_refs(
        &mut self,
        find: &mut dyn FnMut(&PartialNameRef) -> Option<Target>,
        make_entry: &mut dyn FnMut(usize, RefEdit) -> T,
    ) -> Result<(), std::io::Error>;

    /// All processing steps in one and in the correct order.
    ///
    /// Users call this to assure derefs are honored and duplicate checks are done.
    fn pre_process(
        &mut self,
        find: &mut dyn FnMut(&PartialNameRef) -> Option<Target>,
        make_entry: &mut dyn FnMut(usize, RefEdit) -> T,
    ) -> Result<(), std::io::Error> {
        self.extend_with_splits_of_symbolic_refs(find, make_entry)?;
        self.assure_one_name_has_one_edit().map_err(|name| {
            std::io::Error::new(
                std::io::ErrorKind::AlreadyExists,
                format!("A reference named '{name}' has multiple edits"),
            )
        })
    }
}

impl<E> RefEditsExt<E> for Vec<E>
where
    E: std::borrow::Borrow<RefEdit> + std::borrow::BorrowMut<RefEdit>,
{
    fn assure_one_name_has_one_edit(&self) -> Result<(), BString> {
        let mut names: Vec<_> = self.iter().map(|e| &e.borrow().name).collect();
        names.sort();
        match names.windows(2).find(|v| v[0] == v[1]) {
            Some(name) => Err(name[0].as_bstr().to_owned()),
            None => Ok(()),
        }

View on GitHub (pinned to e73179060b)