///| Git Notes operations (Git compatible + Hub integration)
///|
///| Notes are stored in refs/notes/ (Git standard)
///| Default ns is "commits"
///|
/// The notes ref prefix
let notes_ref_prefix : String = "refs/notes/"
///|
/// Add a note to a commit (Git compatible)
pub fn Hub::add_note(
self : Hub,
objects : &@lib.ObjectStore,
refs : &@lib.RefStore,
clock : &@lib.Clock,
commit_id : @bit.ObjectId,
body : String,
author : String,
timestamp : Int64,
ns? : String = "commits",
) -> Note raise @bit.GitError {
ignore(self)
let note = Note::new(commit_id, body, author, timestamp, ns~)
let note_text = note.serialize()
let note_bytes = @utf8.encode(note_text)
let blob_id = objects.put(@bit.ObjectType::Blob, note_bytes)
update_notes_tree(objects, refs, commit_id, blob_id, ns, clock)
note
}
///|
/// Add a plain note (Git standard, no metadata)
pub fn Hub::add_note_plain(
self : Hub,
objects : &@lib.ObjectStore,
refs : &@lib.RefStore,
clock : &@lib.Clock,
commit_id : @bit.ObjectId,
body : String,
ns? : String = "commits",
) -> Unit raise @bit.GitError {
ignore(self)
let body_bytes = @utf8.encode(body)
let blob_id = objects.put(@bit.ObjectType::Blob, body_bytes)
update_notes_tree(objects, refs, commit_id, blob_id, ns, clock)
}
///|
/// Get a note for a commit
pub fn Hub::get_note(
self : Hub,
objects : &@lib.ObjectStore,
refs : &@lib.RefStore,
commit_id : @bit.ObjectId,
ns? : String = "commits",
) -> Note? raise @bit.GitError {
ignore(self)
let text = read_note_blob(objects, refs, commit_id, ns)
match text {
None => None
Some(t) => {
let note = parse_note(t)
if note.ns != ns {
None
} else {
Some(note)
}
}
}
}
///|
/// List all notes in a ns
pub fn Hub::list_notes(
self : Hub,
objects : &@lib.ObjectStore,
refs : &@lib.RefStore,
ns? : String = "commits",
) -> Array[Note] raise @bit.GitError {
ignore(self)
let result : Array[Note] = []
let tree_id = get_notes_tree(objects, refs, ns)
match tree_id {
None => ()
Some(tid) => {
let obj = objects.get(tid)
match obj {
Some(tree_obj) => {
let entries = @bit.parse_tree(tree_obj.data)
for entry in entries {
let blob = objects.get(entry.id)
match blob {
Some(blob_obj) => {
let text = @utf8.decode_lossy(blob_obj.data[:])
let note = parse_note(text)
if note.ns == ns {
result.push(note)
}
}
None => ()
}
}
}
None => ()
}
}
}
result.sort_by(fn(a, b) {
if a.created_at < b.created_at {
-1
} else if a.created_at > b.created_at {
1
} else {
0
}
})
result
}
///|
/// Remove a note from a commit
pub fn Hub::remove_note(
self : Hub,
objects : &@lib.ObjectStore,
refs : &@lib.RefStore,
clock : &@lib.Clock,
commit_id : @bit.ObjectId,
ns? : String = "commits",
) -> Unit raise @bit.GitError {
ignore(self)
remove_notes_tree_entry(objects, refs, commit_id, ns, clock)
}
///|
fn short_hex_note(hex : String, n : Int) -> String {
if hex.length() <= n {
hex
} else {
String::unsafe_substring(hex, start=0, end=n)
}
}
///|
fn get_notes_commit(
refs : &@lib.RefStore,
ns : String,
) -> @bit.ObjectId? raise @bit.GitError {
let ref_name = notes_ref_prefix + ns
refs.resolve(ref_name)
}
///|
fn get_notes_tree(
objects : &@lib.ObjectStore,
refs : &@lib.RefStore,
ns : String,
) -> @bit.ObjectId? raise @bit.GitError {
let commit_id = get_notes_commit(refs, ns)
guard commit_id is Some(cid) else { return None }
let obj = objects.get(cid)
guard obj is Some(commit_obj) else { return None }
let info = @bit.parse_commit(commit_obj.data)
Some(info.tree)
}
///|
fn read_note_blob(
objects : &@lib.ObjectStore,
refs : &@lib.RefStore,
commit_id : @bit.ObjectId,
ns : String,
) -> String? raise @bit.GitError {
let tree_id = get_notes_tree(objects, refs, ns)
guard tree_id is Some(tid) else { return None }
let tree_obj = objects.get(tid)
guard tree_obj is Some(tree) else { return None }
let entries = @bit.parse_tree(tree.data)
let commit_hex = commit_id.to_hex()
for entry in entries {
if entry.name == commit_hex {
let blob = objects.get(entry.id)
match blob {
Some(blob_obj) => return blob_obj.data[:] |> @utf8.decode_lossy |> Some
None => return None
}
}
}
None
}
///|
fn update_notes_tree(
objects : &@lib.ObjectStore,
refs : &@lib.RefStore,
commit_id : @bit.ObjectId,
blob_id : @bit.ObjectId,
ns : String,
clock : &@lib.Clock,
) -> Unit raise @bit.GitError {
let commit_hex = commit_id.to_hex()
// Get existing tree or create empty
let existing_tree_id = get_notes_tree(objects, refs, ns)
let entries : Array[@bit.TreeEntry] = match existing_tree_id {
Some(tid) => {
let obj = objects.get(tid)
match obj {
Some(tree_obj) => @bit.parse_tree(tree_obj.data)
None => []
}
}
None => []
}
// Add or update entry
let new_entries : Array[@bit.TreeEntry] = []
let mut found = false
for entry in entries {
if entry.name == commit_hex {
new_entries.push(@bit.TreeEntry::new("100644", commit_hex, blob_id))
found = true
} else {
new_entries.push(entry)
}
}
if !found {
new_entries.push(@bit.TreeEntry::new("100644", commit_hex, blob_id))
}
new_entries.sort_by((a, b) => String::compare(a.name, b.name))
let tree_bytes = @bit.serialize_tree(new_entries)
let new_tree_id = objects.put(@bit.ObjectType::Tree, tree_bytes)
// Create commit
let parents = match get_notes_commit(refs, ns) {
Some(p) => [p]
None => []
}
let now = clock.now()
let commit = @bit.Commit::new(
new_tree_id,
parents,
"Hub ",
now,
"+0000",
"Hub ",
now,
"+0000",
"Add note for \{short_hex_note(commit_hex, 8)}...\n",
)
let commit_bytes = @bit.serialize_commit_content(commit)
let new_commit_id = objects.put(@bit.ObjectType::Commit, commit_bytes)
// Update ref
let ref_name = notes_ref_prefix + ns
refs.update(ref_name, Some(new_commit_id))
}
///|
fn remove_notes_tree_entry(
objects : &@lib.ObjectStore,
refs : &@lib.RefStore,
commit_id : @bit.ObjectId,
ns : String,
clock : &@lib.Clock,
) -> Unit raise @bit.GitError {
let commit_hex = commit_id.to_hex()
let existing_tree_id = get_notes_tree(objects, refs, ns)
guard existing_tree_id is Some(tid) else { return () }
let obj = objects.get(tid)
guard obj is Some(tree_obj) else { return () }
let entries = @bit.parse_tree(tree_obj.data)
let new_entries : Array[@bit.TreeEntry] = []
let mut found = false
for entry in entries {
if entry.name == commit_hex {
found = true
continue
}
new_entries.push(entry)
}
if !found {
return ()
}
new_entries.sort_by((a, b) => String::compare(a.name, b.name))
let tree_bytes = @bit.serialize_tree(new_entries)
let new_tree_id = objects.put(@bit.ObjectType::Tree, tree_bytes)
let parents = match get_notes_commit(refs, ns) {
Some(p) => [p]
None => []
}
let now = clock.now()
let commit = @bit.Commit::new(
new_tree_id,
parents,
"Hub ",
now,
"+0000",
"Hub ",
now,
"+0000",
"Remove note for \{short_hex_note(commit_hex, 8)}...\n",
)
let commit_bytes = @bit.serialize_commit_content(commit)
let new_commit_id = objects.put(@bit.ObjectType::Commit, commit_bytes)
let ref_name = notes_ref_prefix + ns
refs.update(ref_name, Some(new_commit_id))
}