use crate::error::Result;
use crate::ident::Signature;
use crate::list_limit::{fetch_plus_one, Limited};
use crate::notes::{
lookup_note_blob_oid, materialize_notes_tree, note_object_name, preview_notes_commit,
preview_notes_commit_on_tree, read_blob_bytes, read_notes_snapshot, read_notes_tree_limited,
remove_note_entry, resolve_notes_ref_tree, splice_note_remove, splice_note_upsert,
upsert_note_entry, write_notes_commit_idents, write_notes_commit_idents_on_tree, NotesCommit,
NotesRef, NotesSnapshot, NotesSpliceResult, 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: Signature,
pub author: Option<Signature>,
}
impl NotesWriteOptions {
fn header_lines(&self) -> Result<(String, String)> {
let committer = &self.committer;
let author = self.author.as_ref().unwrap_or(committer);
for signature in [author, committer] {
signature.validate()?;
}
Ok((author.header_line(), committer.header_line()))
}
}
#[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, PartialEq, Eq)]
pub struct NoteRemoveManyPlan {
pub removed: Vec<ObjectId>,
pub notes_ref_tip_before: Option<ObjectId>,
pub notes_ref_tip_after: Option<ObjectId>,
}
struct StagedRemoval {
tip: Option<ObjectId>,
entries: Vec<NotesTreeEntry>,
removed: Vec<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 (_, root_tree) = resolve_notes_ref_tree(self.repo, notes_ref.as_str())?;
let Some(root_tree) = root_tree else {
return Ok(None);
};
let Some(blob_oid) = lookup_note_blob_oid(self.repo, &root_tree, &hex)? else {
return Ok(None);
};
read_blob_bytes(self.repo, &blob_oid).map(Some)
}
pub fn plan_add(
&self,
notes_ref: &NotesRef,
object: &ObjectId,
content: &[u8],
options: &NotesWriteOptions,
force: bool,
) -> Result<NoteAddPlan> {
let (tip, root_tree, outcome) = self.read_for_add(notes_ref, object, force)?;
let _overlay = MemOverlayGuard::enter(self.repo.odb());
let blob = self.repo.odb().write(ObjectKind::Blob, content)?;
let notes_ref_tip_after =
self.preview_add_commit(notes_ref, tip, root_tree, object, blob, options)?;
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 plan = self.plan_remove_many(notes_ref, std::slice::from_ref(object), options)?;
Ok(NoteRemovePlan {
object: *object,
had_note: !plan.removed.is_empty(),
notes_ref_tip_before: plan.notes_ref_tip_before,
notes_ref_tip_after: plan.notes_ref_tip_after,
})
}
pub fn plan_remove_many(
&self,
notes_ref: &NotesRef,
objects: &[ObjectId],
options: &NotesWriteOptions,
) -> Result<NoteRemoveManyPlan> {
let StagedRemoval {
tip,
entries,
removed,
} = self.stage_removals(notes_ref, objects)?;
let notes_ref_tip_after = if removed.is_empty() {
None
} else {
let _overlay = MemOverlayGuard::enter(self.repo.odb());
Some(self.preview_commit(tip, &entries, options)?)
};
Ok(NoteRemoveManyPlan {
removed,
notes_ref_tip_before: tip,
notes_ref_tip_after,
})
}
pub fn add(
&self,
notes_ref: &NotesRef,
object: &ObjectId,
content: &[u8],
options: &NotesWriteOptions,
force: bool,
) -> Result<NoteAddOutcome> {
let (tip, root_tree, outcome) = self.read_for_add(notes_ref, object, force)?;
let blob = self.repo.odb().write(ObjectKind::Blob, content)?;
self.write_add_commit(notes_ref, tip, root_tree, object, blob, options)?;
Ok(outcome)
}
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 (tip, root_tree) = resolve_notes_ref_tree(self.repo, notes_ref.as_str())?;
let Some(mut root_tree) = root_tree else {
return Ok(Vec::new());
};
let mut removed = Vec::new();
for object in objects {
let hex = object.to_hex();
let had = lookup_note_blob_oid(self.repo, &root_tree, &hex)?.is_some();
if !had {
continue;
}
match splice_note_remove(self.repo, root_tree, &hex)? {
NotesSpliceResult::Tree(new_root) => {
root_tree = new_root;
removed.push(*object);
}
NotesSpliceResult::Fallback => {
let StagedRemoval {
tip: staged_tip,
entries,
removed: staged_removed,
} = self.stage_removals(notes_ref, objects)?;
if !staged_removed.is_empty() {
self.write_commit(notes_ref, staged_tip, &entries, options)?;
}
return Ok(staged_removed);
}
}
}
if !removed.is_empty() {
let (author, committer) = options.header_lines()?;
write_notes_commit_idents_on_tree(
self.repo,
notes_ref.as_str(),
tip,
root_tree,
&options.message,
&author,
&committer,
)?;
}
Ok(removed)
}
fn read_for_add(
&self,
notes_ref: &NotesRef,
object: &ObjectId,
force: bool,
) -> Result<(Option<ObjectId>, Option<ObjectId>, NoteAddOutcome)> {
let (tip, root_tree) = resolve_notes_ref_tree(self.repo, notes_ref.as_str())?;
let hex = object.to_hex();
let exists = match root_tree {
Some(tree) => lookup_note_blob_oid(self.repo, &tree, &hex)?.is_some(),
None => false,
};
if exists && !force {
return Err(crate::error::Error::NoteAlreadyExists { object: *object });
}
let outcome = if exists {
NoteAddOutcome::Replaced
} else {
NoteAddOutcome::Added
};
Ok((tip, root_tree, outcome))
}
fn stage_removals(&self, notes_ref: &NotesRef, objects: &[ObjectId]) -> Result<StagedRemoval> {
let NotesSnapshot { tip, mut entries } =
read_notes_snapshot(self.repo, notes_ref.as_str())?;
let removed = objects
.iter()
.filter(|object| remove_note_entry(&mut entries, &object.to_hex()))
.copied()
.collect();
Ok(StagedRemoval {
tip,
entries,
removed,
})
}
fn preview_add_commit(
&self,
notes_ref: &NotesRef,
tip: Option<ObjectId>,
root_tree: Option<ObjectId>,
object: &ObjectId,
blob: ObjectId,
options: &NotesWriteOptions,
) -> Result<ObjectId> {
let tree = self.materialize_add_tree(notes_ref, root_tree, object, blob)?;
let (author, committer) = options.header_lines()?;
let commit = NotesCommit {
message: &options.message,
author: &author,
committer: &committer,
};
preview_notes_commit_on_tree(self.repo, tip, tree, &commit)
}
fn write_add_commit(
&self,
notes_ref: &NotesRef,
tip: Option<ObjectId>,
root_tree: Option<ObjectId>,
object: &ObjectId,
blob: ObjectId,
options: &NotesWriteOptions,
) -> Result<()> {
let tree = self.materialize_add_tree(notes_ref, root_tree, object, blob)?;
let (author, committer) = options.header_lines()?;
write_notes_commit_idents_on_tree(
self.repo,
notes_ref.as_str(),
tip,
tree,
&options.message,
&author,
&committer,
)
}
fn materialize_add_tree(
&self,
notes_ref: &NotesRef,
root_tree: Option<ObjectId>,
object: &ObjectId,
blob: ObjectId,
) -> Result<ObjectId> {
let hex = object.to_hex();
match splice_note_upsert(self.repo, root_tree, &hex, blob)? {
NotesSpliceResult::Tree(tree) => Ok(tree),
NotesSpliceResult::Fallback => {
let NotesSnapshot { mut entries, .. } =
read_notes_snapshot(self.repo, notes_ref.as_str())?;
upsert_note_entry(&mut entries, &hex, blob);
materialize_notes_tree(self.repo, &entries)
}
}
}
fn preview_commit(
&self,
tip: Option<ObjectId>,
entries: &[NotesTreeEntry],
options: &NotesWriteOptions,
) -> Result<ObjectId> {
let (author, committer) = options.header_lines()?;
let commit = NotesCommit {
message: &options.message,
author: &author,
committer: &committer,
};
preview_notes_commit(self.repo, tip, entries, &commit)
}
fn write_commit(
&self,
notes_ref: &NotesRef,
tip: Option<ObjectId>,
entries: &[NotesTreeEntry],
options: &NotesWriteOptions,
) -> Result<()> {
let (author, committer) = options.header_lines()?;
write_notes_commit_idents(
self.repo,
notes_ref.as_str(),
tip,
entries,
&options.message,
&author,
&committer,
)
}
}