//! Access Git notes. use std::borrow::Cow; /// Low-level operations on Git notes trees.pub use gix_note as plumbing; use gix_error::{ErrorExt, ResultExt, message}; use crate::{ Blob, Id, Repository, bstr::{BStr, BString, ByteSlice, ByteVec}, config::tree::{Core, Key, Notes}, ext::ObjectIdExt, refs::{FullName, FullNameRef, PartialName, PartialNameRef, transaction::PreviousValue},}; /// A note and the reference from which it originated.pub struct Note<'platform, 'repo> { /// The source notes reference, such as `refs/notes/commits`. pub reference: &'platform FullNameRef, /// The note blob in any format. pub blob: Blob<'repo>,} /// Cached access to one or more notes references.pub struct Platform<'repo> { pub(crate) repo: &'repo Repository, pub(crate) default_ref: Option<FullName>, // Selected references in lookup order. Each caches its resolved notes-tree ID until a successful local edit // invalidates all resolutions. The selection lives until `with_refs()` replaces it or the platform is dropped. roots: Vec<Root>, // Lazily materialized notes trees, searched by their current root tree ID rather than aligned with `roots`. // Multiple roots resolving to the same tree share one state. States survive root invalidation and selection changes // for the platform's lifetime; a successful edit advances its state so the re-resolved root finds it again. states: Vec<gix_note::State>, commit_message: Option<String>,} /// The existing notes tree and commit, along with the direct reference to update.struct EditRoot { root_tree_id: gix_hash::ObjectId, parent_commit_id: Option<gix_hash::ObjectId>, update_ref: FullName,} impl<'repo> Platform<'repo> { pub(crate) fn new(repo: &'repo Repository) -> Result<Self, crate::Error> { let config = repo.config_snapshot(); let value = config .string(Core::NOTES_REF) .unwrap_or_else(|| Core::NOTES_REF.default_value_or_panic().into()); let default_ref = if value.is_empty() { None } else { Some( FullName::try_from(value) .or_raise(|| message("core.notesRef must be a fully qualified reference name"))?, ) }; let mut refs = default_ref.iter().cloned().collect::<Vec<_>>(); for value in config.plumbing().strings(Notes::DISPLAY_REF).unwrap_or_default() { let display_refs = Notes::DISPLAY_REF .try_into_display_refs(value) .or_raise(|| message("Could not parse notes display references"))?; for pattern in display_refs { add_refs(repo, pattern.as_bstr(), &mut refs)?; } } Ok(Platform { repo, default_ref, roots: refs.into_iter().map(Root::new).collect(), states: Vec::new(), commit_message: None, }) }} /// Builder.impl Platform<'_> { /// Replace the references searched by [`Self::get()`] with `refs`. /// /// Each item is either a fully qualified literal reference such as `refs/notes/review`, or a /// glob pattern. An item containing `*`, `?`, or `[` is treated as a glob and expanded against /// existing references. A literal reference may be absent and is then treated as containing no /// notes. Duplicate references are ignored and the resulting order is preserved. pub fn with_refs(mut self, refs: impl IntoIterator<Item = impl Into<BString>>) -> Result<Self, crate::Error> { let mut selected = Vec::new(); for pattern in refs { let pattern = pattern.into(); add_refs(self.repo, pattern.as_bstr(), &mut selected)?; } self.roots = selected.into_iter().map(Root::new).collect(); Ok(self) } /// Override the commit message used by subsequent note replacements and removals. /// /// Without an override, each operation uses its own gitoxide-specific default message. pub fn with_commit_message(mut self, message: impl Into<String>) -> Self { self.commit_message = Some(message.into()); self }} impl<'repo> Platform<'repo> { /// Return the default notes reference selected by `core.notesRef`, if enabled. /// /// This is distinct from non-default display references searched by [`Self::get()`], /// which may be selected through `notes.displayRef`, `GIT_NOTES_DISPLAY_REF`, or /// [`Self::with_refs()`]. If `core.notesRef` is unset, the default is /// `refs/notes/commits`; an empty value disables the default reference. pub fn default_ref(&self) -> Option<&gix_ref::FullNameRef> { self.default_ref.as_ref().map(AsRef::as_ref) } /// Return the selected notes references in lookup order. /// /// This includes the enabled default reference and any display references selected through /// configuration, the environment, or [`Self::with_refs()`]. A selected literal reference /// may not exist yet, in which case it simply contains no notes. pub fn refs(&self) -> impl Iterator<Item = &gix_ref::FullNameRef> { self.roots.iter().map(|root| root.reference.as_ref()) } /// Return all notes associated with `object` in configured display order. /// /// A notes reference associates at most one note with an object, but the selected /// default and non-default display references may each annotate the same object. /// Consequently, this may return one [`Note`] per selected reference; /// [`Note::reference`] identifies where each note originated. pub fn get<'platform>( &'platform mut self, object: impl Into<gix_hash::ObjectId>, ) -> Result<Vec<Note<'platform, 'repo>>, crate::Error> { let annotated_object_id = object.into(); let mut out = Vec::new(); let repo = self.repo; let (roots, states) = (&mut self.roots, &mut self.states); for selected in roots { let Some(root_tree_id) = selected.tree_id(repo)? else { continue; }; let state = state_for(states, root_tree_id, repo)?; if let Some(note_blob_id) = state .get(&annotated_object_id, repo) .or_raise(|| message!("Could not find notes for {annotated_object_id}"))? { let reference = selected.reference.as_ref(); let blob = repo .find_blob(note_blob_id) .or_raise(|| message!("Could not load note {note_blob_id} from {reference}"))?; out.push(Note { reference, blob }); } } Ok(out) } /// Replace a note in `notes_ref`, assigned to `object`, or add it if absent, returning the previous note id. /// /// * `notes_ref` identifies the notes reference to update. It may be given as `review`, /// `notes/review`, or the fully qualified `refs/notes/review`. If it does not exist, /// it is created. If it is symbolic, its chain is followed and the ultimate direct /// target is updated while the symbolic references are preserved. /// * `object` identifies the Git object to annotate. /// * `data` is stored verbatim in a new blob and used as the note. pub fn replace<N>( &mut self, notes_ref: N, object: impl Into<gix_hash::ObjectId>, data: impl AsRef<[u8]>, ) -> Result<Option<Id<'repo>>, crate::Error> where N: TryInto<PartialName>, N::Error: std::error::Error + Send + Sync + 'static, { let notes_ref = notes_ref .try_into() .or_raise(|| message("The notes reference name is invalid"))?; let notes_ref = expand_notes_ref(notes_ref.as_ref())?; self.replace_at_ref(notes_ref.as_ref(), object, data) } /// Replace a note for `object` with `data` in the fully qualified `notes_ref`, /// or add it if absent, returning the previous note id. /// /// Unlike [`Self::replace()`], this does not apply `refs/notes/` shorthand expansion. pub fn replace_at_ref( &mut self, notes_ref: &gix_ref::FullNameRef, object: impl Into<gix_hash::ObjectId>, data: impl AsRef<[u8]>, ) -> Result<Option<Id<'repo>>, crate::Error> { let EditRoot { root_tree_id, parent_commit_id, update_ref, } = self.lookup_edit_root(notes_ref)?; let annotated_object_id = object.into(); let note_blob_id = self .repo .write_blob(data.as_ref()) .or_raise(|| message("Could not write note blob"))? .detach(); let edit = { let state = state_for(&mut self.states, root_tree_id, self.repo)?; state.replace(annotated_object_id, note_blob_id, self.repo) } .or_raise(|| message!("Could not replace note for {annotated_object_id}"))?; self.commit_edit( update_ref.as_ref(), parent_commit_id, edit, "Notes replaced by gitoxide", )?; Ok(edit.previous.map(|id| id.attach(self.repo))) } /// Remove a note in `notes_ref`, returning the removed note id. /// /// * `notes_ref` identifies the notes reference to update. It may be given as `review`, /// `notes/review`, or the fully qualified `refs/notes/review`. If it does not exist, /// removal is a no-op and the reference remains absent. If it is symbolic, its chain is /// followed and the ultimate direct target is updated while the symbolic references are /// preserved. Removing the last note from an existing reference retains it, pointing to /// a new commit with an empty tree. /// * `object` identifies the Git object whose note should be removed. pub fn remove<N>( &mut self, notes_ref: N, object: impl Into<gix_hash::ObjectId>, ) -> Result<Option<Id<'repo>>, crate::Error> where N: TryInto<PartialName>, N::Error: std::error::Error + Send + Sync + 'static, { let notes_ref = notes_ref .try_into() .or_raise(|| message("The notes reference name is invalid"))?; let notes_ref = expand_notes_ref(notes_ref.as_ref())?; let EditRoot { root_tree_id, parent_commit_id, update_ref, } = self.lookup_edit_root(notes_ref.as_ref())?; let annotated_object_id = object.into(); let edit = { let state = state_for(&mut self.states, root_tree_id, self.repo)?; state.remove(annotated_object_id, self.repo) } .or_raise(|| message!("Could not remove note for {annotated_object_id}"))?; if edit.previous.is_some() { self.commit_edit(update_ref.as_ref(), parent_commit_id, edit, "Notes removed by gitoxide")?; } Ok(edit.previous.map(|id| id.attach(self.repo))) } /// Return the notes tree and commit to edit, and the direct reference to update. fn lookup_edit_root(&self, notes_ref: &gix_ref::FullNameRef) -> Result<EditRoot, crate::Error> { match self .repo .try_find_reference(notes_ref) .or_raise(|| message!("Could not find notes reference {notes_ref}"))? { Some(mut reference) => { // TODO(gix-error): write object-chained style (.object().try_into_commit()?) once // the underlying error is Exn (which converts) automatically. let parent_commit_id = reference .follow_to_object() .or_raise(|| message!("Could not follow notes reference {notes_ref} to an object"))? .detach(); let root_tree_id = self .repo .find_commit(parent_commit_id) .or_raise(|| message!("Notes reference {notes_ref} must point to a commit"))? .tree_id() .or_raise(|| message!("Could not read the tree of notes commit {parent_commit_id}"))? .detach(); Ok(EditRoot { root_tree_id, parent_commit_id: Some(parent_commit_id), update_ref: reference.name().to_owned(), }) } None => Ok(EditRoot { root_tree_id: gix_hash::ObjectId::empty_tree(self.repo.object_hash()), parent_commit_id: None, update_ref: notes_ref.to_owned(), }), } } /// Commit `edit` to the resolved direct `notes_ref` and invalidate all cached roots. /// /// Create the reference if `parent_commit_id` is `None`; otherwise update it only if it still points to that commit. fn commit_edit( &mut self, notes_ref: &gix_ref::FullNameRef, parent_commit_id: Option<gix_hash::ObjectId>, edit: gix_note::Edit, default_message: &str, ) -> Result<(), crate::Error> { let message = self.commit_message.as_deref().unwrap_or(default_message); let commit = self .repo .new_commit(message, edit.tree, parent_commit_id) .or_raise(|| message!("Could not create commit for {notes_ref}"))?; let expected = parent_commit_id.map_or(PreviousValue::MustNotExist, |commit_id| { PreviousValue::MustExistAndMatch(gix_ref::Target::Object(commit_id)) }); self.repo .reference(notes_ref, commit.id, expected, format!("notes: {message}")) .or_raise(|| message!("Could not update notes reference {notes_ref}"))?; // Any selected reference may resolve through a symbolic chain to the direct reference just updated, and the // cached root does not record that relationship. Invalidating every root prevents aliases from serving stale // notes; as a trade-off, the next lookup resolves each selected reference and peels its notes tree again. for root in &mut self.roots { root.tree_id = None; } Ok(()) }} fn state_for<'a>( states: &'a mut Vec<gix_note::State>, root_tree_id: gix_hash::ObjectId, repo: &Repository,) -> Result<&'a mut gix_note::State, crate::Error> { if let Some(index) = states.iter().position(|state| state.root_tree_id() == root_tree_id) { return Ok(&mut states[index]); } let index = states.len(); states.push( gix_note::State::new(root_tree_id, repo) .or_raise(|| message!("Could not initialize notes tree {root_tree_id}"))?, ); Ok(&mut states[index])} /// A selected notes reference and its lazily resolved root tree.struct Root { /// The selected notes reference, such as `refs/notes/commits`. reference: FullName, /// `None` means unresolved, `Some(None)` means the reference is absent, and /// `Some(Some(id))` caches its resolved root tree. tree_id: Option<Option<gix_hash::ObjectId>>,} impl Root { fn new(reference: FullName) -> Self { Root { reference, tree_id: None, } } fn tree_id(&mut self, repo: &Repository) -> Result<Option<gix_hash::ObjectId>, crate::Error> { if let Some(root_tree_id) = self.tree_id { return Ok(root_tree_id); } let name = &self.reference; let root_tree_id = match repo .try_find_reference(name.as_ref()) .or_raise(|| message!("Could not find notes reference {name}"))? { Some(mut reference) => Some( reference .peel_to_tree() .or_raise(|| message!("Could not peel notes reference {name} to a tree"))? .id, ), None => None, }; self.tree_id = Some(root_tree_id); Ok(root_tree_id) }} fn add_refs(repo: &Repository, pattern: &BStr, out: &mut Vec<FullName>) -> Result<(), crate::Error> { let mut push_unique = |reference| { if !out.contains(&reference) { out.push(reference); } }; let parsed = gix_glob::Pattern::from_bytes_without_negation(pattern) .ok_or_else(|| message("Notes display references must not be empty").raise())?; if parsed .mode .intersects(gix_glob::pattern::Mode::ABSOLUTE | gix_glob::pattern::Mode::MUST_BE_DIR) { return Ok(()); } if parsed.has_wildcard() { let platform = repo .references() .or_raise(|| message!("Could not iterate notes references matching {pattern}"))?; let references = platform .all() .or_raise(|| message!("Could not iterate notes references matching {pattern}"))?; for reference in references { let reference = reference .map_err(gix_error::Error::from_boxed) .or_raise(|| message!("Could not read notes reference matching {pattern}"))?; if parsed.matches(reference.name().as_bstr(), gix_glob::wildmatch::Mode::empty()) { push_unique(reference.inner.name); } } } else { push_unique( FullName::try_from(pattern) .or_raise(|| message!("Notes display reference {pattern} is not fully qualified"))?, ); } Ok(())} fn expand_notes_ref(name: &PartialNameRef) -> Result<Cow<'_, FullNameRef>, gix_error::Exn<gix_ref::name::Error>> { let name = name.as_bstr(); if name.starts_with_str("refs/notes/") { return Ok(Cow::Borrowed(name.try_into()?)); } let mut name = name.to_owned(); if name.starts_with_str("notes/") { name.insert_str(0, "refs/"); } else { name.insert_str(0, "refs/notes/"); } Ok(Cow::Owned(name.try_into()?))}
//! Access Git notes.
use std::borrow::Cow;
/// Low-level operations on Git notes trees.
pub use gix_note as plumbing;
use gix_error::{ErrorExt, ResultExt, message};
use crate::{
Blob, Id, Repository,
bstr::{BStr, BString, ByteSlice, ByteVec},
config::tree::{Core, Key, Notes},
ext::ObjectIdExt,
refs::{FullName, FullNameRef, PartialName, PartialNameRef, transaction::PreviousValue},
};
/// A note and the reference from which it originated.
pub struct Note<'platform, 'repo> {
/// The source notes reference, such as `refs/notes/commits`.
pub reference: &'platform FullNameRef,
/// The note blob in any format.
pub blob: Blob<'repo>,
}
/// Cached access to one or more notes references.
pub struct Platform<'repo> {
pub(crate) repo: &'repo Repository,
pub(crate) default_ref: Option<FullName>,
// Selected references in lookup order. Each caches its resolved notes-tree ID until a successful local edit
// invalidates all resolutions. The selection lives until `with_refs()` replaces it or the platform is dropped.
roots: Vec<Root>,
// Lazily materialized notes trees, searched by their current root tree ID rather than aligned with `roots`.
// Multiple roots resolving to the same tree share one state. States survive root invalidation and selection changes
// for the platform's lifetime; a successful edit advances its state so the re-resolved root finds it again.
states: Vec<gix_note::State>,
commit_message: Option<String>,
}
/// The existing notes tree and commit, along with the direct reference to update.
struct EditRoot {
root_tree_id: gix_hash::ObjectId,
parent_commit_id: Option<gix_hash::ObjectId>,
update_ref: FullName,
}
impl<'repo> Platform<'repo> {
pub(crate) fn new(repo: &'repo Repository) -> Result<Self, crate::Error> {
let config = repo.config_snapshot();
let value = config
.string(Core::NOTES_REF)
.unwrap_or_else(|| Core::NOTES_REF.default_value_or_panic().into());
let default_ref = if value.is_empty() {
None
} else {
Some(
FullName::try_from(value)
.or_raise(|| message("core.notesRef must be a fully qualified reference name"))?,
)
};
let mut refs = default_ref.iter().cloned().collect::<Vec<_>>();
for value in config.plumbing().strings(Notes::DISPLAY_REF).unwrap_or_default() {
let display_refs = Notes::DISPLAY_REF
.try_into_display_refs(value)
.or_raise(|| message("Could not parse notes display references"))?;
for pattern in display_refs {
add_refs(repo, pattern.as_bstr(), &mut refs)?;
}
}
Ok(Platform {
repo,
default_ref,
roots: refs.into_iter().map(Root::new).collect(),
states: Vec::new(),
commit_message: None,
})
}
}
/// Builder.
impl Platform<'_> {
/// Replace the references searched by [`Self::get()`] with `refs`.
///
/// Each item is either a fully qualified literal reference such as `refs/notes/review`, or a
/// glob pattern. An item containing `*`, `?`, or `[` is treated as a glob and expanded against
/// existing references. A literal reference may be absent and is then treated as containing no
/// notes. Duplicate references are ignored and the resulting order is preserved.
pub fn with_refs(mut self, refs: impl IntoIterator<Item = impl Into<BString>>) -> Result<Self, crate::Error> {
let mut selected = Vec::new();
for pattern in refs {
let pattern = pattern.into();
add_refs(self.repo, pattern.as_bstr(), &mut selected)?;
}
self.roots = selected.into_iter().map(Root::new).collect();
Ok(self)
}
/// Override the commit message used by subsequent note replacements and removals.
///
/// Without an override, each operation uses its own gitoxide-specific default message.
pub fn with_commit_message(mut self, message: impl Into<String>) -> Self {
self.commit_message = Some(message.into());
self
}
}
impl<'repo> Platform<'repo> {
/// Return the default notes reference selected by `core.notesRef`, if enabled.
///
/// This is distinct from non-default display references searched by [`Self::get()`],
/// which may be selected through `notes.displayRef`, `GIT_NOTES_DISPLAY_REF`, or
/// [`Self::with_refs()`]. If `core.notesRef` is unset, the default is
/// `refs/notes/commits`; an empty value disables the default reference.
pub fn default_ref(&self) -> Option<&gix_ref::FullNameRef> {
self.default_ref.as_ref().map(AsRef::as_ref)
}
/// Return the selected notes references in lookup order.
///
/// This includes the enabled default reference and any display references selected through
/// configuration, the environment, or [`Self::with_refs()`]. A selected literal reference
/// may not exist yet, in which case it simply contains no notes.
pub fn refs(&self) -> impl Iterator<Item = &gix_ref::FullNameRef> {
self.roots.iter().map(|root| root.reference.as_ref())
}
/// Return all notes associated with `object` in configured display order.
///
/// A notes reference associates at most one note with an object, but the selected
/// default and non-default display references may each annotate the same object.
/// Consequently, this may return one [`Note`] per selected reference;
/// [`Note::reference`] identifies where each note originated.
pub fn get<'platform>(
&'platform mut self,
object: impl Into<gix_hash::ObjectId>,
) -> Result<Vec<Note<'platform, 'repo>>, crate::Error> {
let annotated_object_id = object.into();
let mut out = Vec::new();
let repo = self.repo;
let (roots, states) = (&mut self.roots, &mut self.states);
for selected in roots {
let Some(root_tree_id) = selected.tree_id(repo)? else {
continue;
};
let state = state_for(states, root_tree_id, repo)?;
if let Some(note_blob_id) = state
.get(&annotated_object_id, repo)
.or_raise(|| message!("Could not find notes for {annotated_object_id}"))?
{
let reference = selected.reference.as_ref();
let blob = repo
.find_blob(note_blob_id)
.or_raise(|| message!("Could not load note {note_blob_id} from {reference}"))?;
out.push(Note { reference, blob });
}
}
Ok(out)
}
/// Replace a note in `notes_ref`, assigned to `object`, or add it if absent, returning the previous note id.
///
/// * `notes_ref` identifies the notes reference to update. It may be given as `review`,
/// `notes/review`, or the fully qualified `refs/notes/review`. If it does not exist,
/// it is created. If it is symbolic, its chain is followed and the ultimate direct
/// target is updated while the symbolic references are preserved.
/// * `object` identifies the Git object to annotate.
/// * `data` is stored verbatim in a new blob and used as the note.
pub fn replace<N>(
&mut self,
notes_ref: N,
object: impl Into<gix_hash::ObjectId>,
data: impl AsRef<[u8]>,
) -> Result<Option<Id<'repo>>, crate::Error>
where
N: TryInto<PartialName>,
N::Error: std::error::Error + Send + Sync + 'static,
{
let notes_ref = notes_ref
.try_into()
.or_raise(|| message("The notes reference name is invalid"))?;
let notes_ref = expand_notes_ref(notes_ref.as_ref())?;
self.replace_at_ref(notes_ref.as_ref(), object, data)
}
/// Replace a note for `object` with `data` in the fully qualified `notes_ref`,
/// or add it if absent, returning the previous note id.
///
/// Unlike [`Self::replace()`], this does not apply `refs/notes/` shorthand expansion.
pub fn replace_at_ref(
&mut self,
notes_ref: &gix_ref::FullNameRef,
object: impl Into<gix_hash::ObjectId>,
data: impl AsRef<[u8]>,
) -> Result<Option<Id<'repo>>, crate::Error> {
let EditRoot {
root_tree_id,
parent_commit_id,
update_ref,
} = self.lookup_edit_root(notes_ref)?;
let annotated_object_id = object.into();
let note_blob_id = self
.repo
.write_blob(data.as_ref())
.or_raise(|| message("Could not write note blob"))?
.detach();
let edit = {
let state = state_for(&mut self.states, root_tree_id, self.repo)?;
state.replace(annotated_object_id, note_blob_id, self.repo)
}
.or_raise(|| message!("Could not replace note for {annotated_object_id}"))?;
self.commit_edit(
update_ref.as_ref(),
parent_commit_id,
edit,
"Notes replaced by gitoxide",
)?;
Ok(edit.previous.map(|id| id.attach(self.repo)))
}
/// Remove a note in `notes_ref`, returning the removed note id.
///
/// * `notes_ref` identifies the notes reference to update. It may be given as `review`,
/// `notes/review`, or the fully qualified `refs/notes/review`. If it does not exist,
/// removal is a no-op and the reference remains absent. If it is symbolic, its chain is
/// followed and the ultimate direct target is updated while the symbolic references are
/// preserved. Removing the last note from an existing reference retains it, pointing to
/// a new commit with an empty tree.
/// * `object` identifies the Git object whose note should be removed.
pub fn remove<N>(
&mut self,
notes_ref: N,
object: impl Into<gix_hash::ObjectId>,
) -> Result<Option<Id<'repo>>, crate::Error>
where
N: TryInto<PartialName>,
N::Error: std::error::Error + Send + Sync + 'static,
{
let notes_ref = notes_ref
.try_into()
.or_raise(|| message("The notes reference name is invalid"))?;
let notes_ref = expand_notes_ref(notes_ref.as_ref())?;
let EditRoot {
root_tree_id,
parent_commit_id,
update_ref,
} = self.lookup_edit_root(notes_ref.as_ref())?;
let annotated_object_id = object.into();
let edit = {
let state = state_for(&mut self.states, root_tree_id, self.repo)?;
state.remove(annotated_object_id, self.repo)
}
.or_raise(|| message!("Could not remove note for {annotated_object_id}"))?;
if edit.previous.is_some() {
self.commit_edit(update_ref.as_ref(), parent_commit_id, edit, "Notes removed by gitoxide")?;
}
Ok(edit.previous.map(|id| id.attach(self.repo)))
}
/// Return the notes tree and commit to edit, and the direct reference to update.
fn lookup_edit_root(&self, notes_ref: &gix_ref::FullNameRef) -> Result<EditRoot, crate::Error> {
match self
.repo
.try_find_reference(notes_ref)
.or_raise(|| message!("Could not find notes reference {notes_ref}"))?
{
Some(mut reference) => {
// TODO(gix-error): write object-chained style (.object().try_into_commit()?) once
// the underlying error is Exn (which converts) automatically.
let parent_commit_id = reference
.follow_to_object()
.or_raise(|| message!("Could not follow notes reference {notes_ref} to an object"))?
.detach();
let root_tree_id = self
.repo
.find_commit(parent_commit_id)
.or_raise(|| message!("Notes reference {notes_ref} must point to a commit"))?
.tree_id()
.or_raise(|| message!("Could not read the tree of notes commit {parent_commit_id}"))?
.detach();
Ok(EditRoot {
root_tree_id,
parent_commit_id: Some(parent_commit_id),
update_ref: reference.name().to_owned(),
})
}
None => Ok(EditRoot {
root_tree_id: gix_hash::ObjectId::empty_tree(self.repo.object_hash()),
parent_commit_id: None,
update_ref: notes_ref.to_owned(),
}),
}
}
/// Commit `edit` to the resolved direct `notes_ref` and invalidate all cached roots.
///
/// Create the reference if `parent_commit_id` is `None`; otherwise update it only if it still points to that commit.
fn commit_edit(
&mut self,
notes_ref: &gix_ref::FullNameRef,
parent_commit_id: Option<gix_hash::ObjectId>,
edit: gix_note::Edit,
default_message: &str,
) -> Result<(), crate::Error> {
let message = self.commit_message.as_deref().unwrap_or(default_message);
let commit = self
.repo
.new_commit(message, edit.tree, parent_commit_id)
.or_raise(|| message!("Could not create commit for {notes_ref}"))?;
let expected = parent_commit_id.map_or(PreviousValue::MustNotExist, |commit_id| {
PreviousValue::MustExistAndMatch(gix_ref::Target::Object(commit_id))
});
self.repo
.reference(notes_ref, commit.id, expected, format!("notes: {message}"))
.or_raise(|| message!("Could not update notes reference {notes_ref}"))?;
// Any selected reference may resolve through a symbolic chain to the direct reference just updated, and the
// cached root does not record that relationship. Invalidating every root prevents aliases from serving stale
// notes; as a trade-off, the next lookup resolves each selected reference and peels its notes tree again.
for root in &mut self.roots {
root.tree_id = None;
}
Ok(())
}
}
fn state_for<'a>(
states: &'a mut Vec<gix_note::State>,
root_tree_id: gix_hash::ObjectId,
repo: &Repository,
) -> Result<&'a mut gix_note::State, crate::Error> {
if let Some(index) = states.iter().position(|state| state.root_tree_id() == root_tree_id) {
return Ok(&mut states[index]);
}
let index = states.len();
states.push(
gix_note::State::new(root_tree_id, repo)
.or_raise(|| message!("Could not initialize notes tree {root_tree_id}"))?,
);
Ok(&mut states[index])
}
/// A selected notes reference and its lazily resolved root tree.
struct Root {
/// The selected notes reference, such as `refs/notes/commits`.
reference: FullName,
/// `None` means unresolved, `Some(None)` means the reference is absent, and
/// `Some(Some(id))` caches its resolved root tree.
tree_id: Option<Option<gix_hash::ObjectId>>,
}
impl Root {
fn new(reference: FullName) -> Self {
Root {
reference,
tree_id: None,
}
}
fn tree_id(&mut self, repo: &Repository) -> Result<Option<gix_hash::ObjectId>, crate::Error> {
if let Some(root_tree_id) = self.tree_id {
return Ok(root_tree_id);
}
let name = &self.reference;
let root_tree_id = match repo
.try_find_reference(name.as_ref())
.or_raise(|| message!("Could not find notes reference {name}"))?
{
Some(mut reference) => Some(
reference
.peel_to_tree()
.or_raise(|| message!("Could not peel notes reference {name} to a tree"))?
.id,
),
None => None,
};
self.tree_id = Some(root_tree_id);
Ok(root_tree_id)
}
}
fn add_refs(repo: &Repository, pattern: &BStr, out: &mut Vec<FullName>) -> Result<(), crate::Error> {
let mut push_unique = |reference| {
if !out.contains(&reference) {
out.push(reference);
}
};
let parsed = gix_glob::Pattern::from_bytes_without_negation(pattern)
.ok_or_else(|| message("Notes display references must not be empty").raise())?;
if parsed
.mode
.intersects(gix_glob::pattern::Mode::ABSOLUTE | gix_glob::pattern::Mode::MUST_BE_DIR)
{
return Ok(());
}
if parsed.has_wildcard() {
let platform = repo
.references()
.or_raise(|| message!("Could not iterate notes references matching {pattern}"))?;
let references = platform
.all()
.or_raise(|| message!("Could not iterate notes references matching {pattern}"))?;
for reference in references {
let reference = reference
.map_err(gix_error::Error::from_boxed)
.or_raise(|| message!("Could not read notes reference matching {pattern}"))?;
if parsed.matches(reference.name().as_bstr(), gix_glob::wildmatch::Mode::empty()) {
push_unique(reference.inner.name);
}
}
} else {
push_unique(
FullName::try_from(pattern)
.or_raise(|| message!("Notes display reference {pattern} is not fully qualified"))?,
);
}
Ok(())
}
fn expand_notes_ref(name: &PartialNameRef) -> Result<Cow<'_, FullNameRef>, gix_error::Exn<gix_ref::name::Error>> {
let name = name.as_bstr();
if name.starts_with_str("refs/notes/") {
return Ok(Cow::Borrowed(name.try_into()?));
}
let mut name = name.to_owned();
if name.starts_with_str("notes/") {
name.insert_str(0, "refs/");
} else {
name.insert_str(0, "refs/notes/");
}
Ok(Cow::Owned(name.try_into()?))
}