use crate::error::Result;
use crate::list_limit::{fetch_plus_one, Limited};
use crate::notes::{
note_object_name, preview_notes_commit, read_blob_bytes, read_notes_snapshot, read_notes_tree,
read_notes_tree_limited, remove_note_entry, upsert_note_entry, write_notes_commit_idents,
NotesCommit, NotesRef, NotesSnapshot, NotesTreeEntry,
};
use crate::objects::{ObjectId, ObjectKind};
use crate::porcelain::merge::MemOverlayGuard;
use crate::repo::Repository;
#[derive(Debug, Clone)]
pub struct NotesWriteOptions {
pub message: String,
pub committer: String,
pub author: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NoteListing {
pub object: ObjectId,
pub note_blob: ObjectId,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Note {
pub object: ObjectId,
pub note_blob: ObjectId,
pub body: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NoteAddPlan {
pub object: ObjectId,
pub outcome: NoteAddOutcome,
pub notes_ref_tip_before: Option<ObjectId>,
pub notes_ref_tip_after: ObjectId,
pub note_blob: ObjectId,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NoteRemovePlan {
pub object: ObjectId,
pub had_note: bool,
pub notes_ref_tip_before: Option<ObjectId>,
pub notes_ref_tip_after: Option<ObjectId>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NoteAddOutcome {
Added,
Replaced,
}
fn listings(entries: Vec<NotesTreeEntry>) -> impl Iterator<Item = NoteListing> {
entries.into_iter().filter_map(|e| {
let object = note_object_name(&e.path)?.parse().ok()?;
Some(NoteListing {
object,
note_blob: e.oid,
})
})
}
#[derive(Debug, Copy, Clone)]
pub struct RepositoryNotes<'repo> {
repo: &'repo Repository,
}
impl<'repo> RepositoryNotes<'repo> {
pub(crate) fn new(repo: &'repo Repository) -> Self {
Self { repo }
}
#[must_use]
pub fn repository(&self) -> &'repo Repository {
self.repo
}
pub fn list(&self, notes_ref: &NotesRef, limit: Option<usize>) -> Result<Limited<NoteListing>> {
let cap = fetch_plus_one(limit);
let (entries, stopped_early) = read_notes_tree_limited(self.repo, notes_ref.as_str(), cap)?;
let notes: Vec<NoteListing> = listings(entries).collect();
let mut limited = Limited::from_vec(notes, limit);
if stopped_early {
limited.truncated = true;
}
Ok(limited)
}
pub fn list_with_bodies(
&self,
notes_ref: &NotesRef,
limit: Option<usize>,
) -> Result<Limited<Note>> {
let listed = self.list(notes_ref, limit)?;
let mut notes = Vec::with_capacity(listed.items.len());
for listing in listed.items {
notes.push(Note {
body: read_blob_bytes(self.repo, &listing.note_blob)?,
object: listing.object,
note_blob: listing.note_blob,
});
}
notes.sort_by_key(|note| note.object);
Ok(Limited {
items: notes,
truncated: listed.truncated,
})
}
pub fn read(&self, notes_ref: &NotesRef, object: &ObjectId) -> Result<Option<Vec<u8>>> {
let hex = object.to_hex();
let entries = read_notes_tree(self.repo, notes_ref.as_str())?;
let Some(entry) = entries
.iter()
.find(|e| note_object_name(&e.path).as_deref() == Some(hex.as_str()))
else {
return Ok(None);
};
read_blob_bytes(self.repo, &entry.oid).map(Some)
}
pub fn plan_add(
&self,
notes_ref: &NotesRef,
object: &ObjectId,
content: &[u8],
options: &NotesWriteOptions,
force: bool,
) -> Result<NoteAddPlan> {
let hex = object.to_hex();
let NotesSnapshot { tip, mut entries } =
read_notes_snapshot(self.repo, notes_ref.as_str())?;
let exists = entries
.iter()
.any(|e| note_object_name(&e.path).as_deref() == Some(hex.as_str()));
if exists && !force {
return Err(crate::error::Error::NoteAlreadyExists { object: *object });
}
let outcome = if exists {
NoteAddOutcome::Replaced
} else {
NoteAddOutcome::Added
};
let _overlay = MemOverlayGuard::enter(self.repo.odb());
let blob = self.repo.odb().write(ObjectKind::Blob, content)?;
upsert_note_entry(&mut entries, &hex, blob);
let commit = NotesCommit {
message: &options.message,
author: options.author.as_deref().unwrap_or(&options.committer),
committer: &options.committer,
};
let notes_ref_tip_after = preview_notes_commit(self.repo, tip, &entries, &commit)?;
Ok(NoteAddPlan {
object: *object,
outcome,
notes_ref_tip_before: tip,
notes_ref_tip_after,
note_blob: blob,
})
}
pub fn plan_remove(
&self,
notes_ref: &NotesRef,
object: &ObjectId,
options: &NotesWriteOptions,
) -> Result<NoteRemovePlan> {
let NotesSnapshot { tip, mut entries } =
read_notes_snapshot(self.repo, notes_ref.as_str())?;
let had_note = remove_note_entry(&mut entries, &object.to_hex());
if !had_note {
return Ok(NoteRemovePlan {
object: *object,
had_note: false,
notes_ref_tip_before: tip,
notes_ref_tip_after: None,
});
}
let _overlay = MemOverlayGuard::enter(self.repo.odb());
let commit = NotesCommit {
message: &options.message,
author: options.author.as_deref().unwrap_or(&options.committer),
committer: &options.committer,
};
let notes_ref_tip_after = preview_notes_commit(self.repo, tip, &entries, &commit)?;
Ok(NoteRemovePlan {
object: *object,
had_note: true,
notes_ref_tip_before: tip,
notes_ref_tip_after: Some(notes_ref_tip_after),
})
}
pub fn add(
&self,
notes_ref: &NotesRef,
object: &ObjectId,
content: &[u8],
options: &NotesWriteOptions,
force: bool,
) -> Result<NoteAddOutcome> {
let hex = object.to_hex();
let NotesSnapshot { tip, mut entries } =
read_notes_snapshot(self.repo, notes_ref.as_str())?;
let exists = entries
.iter()
.any(|e| note_object_name(&e.path).as_deref() == Some(hex.as_str()));
if exists && !force {
return Err(crate::error::Error::NoteAlreadyExists { object: *object });
}
let blob = self.repo.odb().write(ObjectKind::Blob, content)?;
upsert_note_entry(&mut entries, &hex, blob);
write_notes_commit_idents(
self.repo,
notes_ref.as_str(),
tip,
&entries,
&options.message,
options.author.as_deref().unwrap_or(&options.committer),
&options.committer,
)?;
Ok(if exists {
NoteAddOutcome::Replaced
} else {
NoteAddOutcome::Added
})
}
pub fn remove(
&self,
notes_ref: &NotesRef,
object: &ObjectId,
options: &NotesWriteOptions,
) -> Result<()> {
self.remove_many(notes_ref, std::slice::from_ref(object), options)?;
Ok(())
}
pub fn remove_many(
&self,
notes_ref: &NotesRef,
objects: &[ObjectId],
options: &NotesWriteOptions,
) -> Result<Vec<ObjectId>> {
let NotesSnapshot { tip, mut entries } =
read_notes_snapshot(self.repo, notes_ref.as_str())?;
let mut removed = Vec::new();
for object in objects {
if remove_note_entry(&mut entries, &object.to_hex()) {
removed.push(*object);
}
}
if removed.is_empty() {
return Ok(removed);
}
write_notes_commit_idents(
self.repo,
notes_ref.as_str(),
tip,
&entries,
&options.message,
options.author.as_deref().unwrap_or(&options.committer),
&options.committer,
)?;
Ok(removed)
}
}