1use std::borrow::Cow;
16use std::collections::{BTreeMap, BTreeSet, HashMap};
17use std::fs;
18
19use merge3::{Merge3, StandardMarkers};
20use time::OffsetDateTime;
21
22use crate::config::ConfigSet;
23use crate::diff::zero_oid;
24use crate::error::{Error, Result};
25use crate::merge_base::merge_bases_first_vs_rest;
26use crate::objects::{
27 parse_commit, parse_tree, serialize_commit, serialize_tree, tree_entry_cmp, CommitData,
28 ObjectId, ObjectKind, TreeEntry,
29};
30use crate::refs::{append_reflog, resolve_ref, should_autocreate_reflog, write_ref};
31use crate::repo::Repository;
32use crate::rev_parse::resolve_revision;
33
34pub const NOTES_MERGE_WORKTREE: &str = "NOTES_MERGE_WORKTREE";
36
37#[derive(Clone)]
38pub struct NotesTreeEntry {
39 pub mode: u32,
40 pub path: Vec<u8>,
41 pub oid: ObjectId,
42}
43
44enum NotesTreeChild {
45 Blob { mode: u32, oid: ObjectId },
46 Tree(Vec<NotesTreeEntry>),
47}
48
49pub fn note_object_name(path: &[u8]) -> Option<String> {
50 let compact: Vec<u8> = path.iter().copied().filter(|byte| *byte != b'/').collect();
51 if compact.len() != 40 || !compact.iter().all(u8::is_ascii_hexdigit) {
52 return None;
53 }
54 String::from_utf8(compact)
55 .ok()
56 .map(|name| name.to_ascii_lowercase())
57}
58
59pub fn display_note_path(entry: &NotesTreeEntry) -> Cow<'_, str> {
60 if let Some(name) = note_object_name(&entry.path) {
61 Cow::Owned(name)
62 } else {
63 String::from_utf8_lossy(&entry.path)
64 }
65}
66
67fn collect_notes_tree_entries(
68 repo: &Repository,
69 tree_oid: &ObjectId,
70 prefix: &[u8],
71 out: &mut Vec<NotesTreeEntry>,
72) -> Result<()> {
73 let tree_obj = repo.odb.read(tree_oid)?;
74 if tree_obj.kind != ObjectKind::Tree {
75 return Err(Error::Message("notes commit has invalid tree".into()));
76 }
77
78 for entry in parse_tree(&tree_obj.data)? {
79 let mut path = prefix.to_vec();
80 if !path.is_empty() {
81 path.push(b'/');
82 }
83 path.extend_from_slice(&entry.name);
84
85 if entry.mode == 0o040000 {
86 collect_notes_tree_entries(repo, &entry.oid, &path, out)?;
87 } else {
88 out.push(NotesTreeEntry {
89 mode: entry.mode,
90 path,
91 oid: entry.oid,
92 });
93 }
94 }
95
96 Ok(())
97}
98
99pub fn read_notes_tree(repo: &Repository, notes_ref: &str) -> Result<Vec<NotesTreeEntry>> {
102 let tree_oid = match resolve_ref(&repo.git_dir, notes_ref) {
103 Ok(oid) => {
104 let obj = repo.odb.read(&oid)?;
105 match obj.kind {
106 ObjectKind::Commit => parse_commit(&obj.data)?.tree,
107 ObjectKind::Tree => oid,
108 _ => {
109 return Err(Error::Message(format!(
110 "{notes_ref} does not point to a commit or tree"
111 )))
112 }
113 }
114 }
115 Err(_) => {
116 let oid = match resolve_revision(repo, notes_ref) {
117 Ok(o) => o,
118 Err(_) => return Ok(Vec::new()),
119 };
120 let obj = repo.odb.read(&oid)?;
121 match obj.kind {
122 ObjectKind::Commit => parse_commit(&obj.data)?.tree,
123 ObjectKind::Tree => oid,
124 _ => {
125 return Err(Error::Message(format!(
126 "{notes_ref} does not point to a commit or tree"
127 )))
128 }
129 }
130 }
131 };
132 let mut entries = Vec::new();
133 collect_notes_tree_entries(repo, &tree_oid, b"", &mut entries)?;
134 Ok(entries)
135}
136
137fn notes_fanout(entries: &[NotesTreeEntry]) -> usize {
138 let mut note_count = entries
139 .iter()
140 .filter(|entry| note_object_name(&entry.path).is_some())
141 .count();
142 let mut fanout = 0usize;
143 while note_count > 0xff {
144 note_count >>= 8;
145 fanout += 1;
146 }
147 fanout
148}
149
150fn path_with_fanout(hex: &str, fanout: usize) -> Vec<u8> {
151 let mut path = Vec::with_capacity(hex.len() + fanout);
152 let bytes = hex.as_bytes();
153 let split = fanout.min(bytes.len() / 2);
154 for idx in 0..split {
155 let start = idx * 2;
156 path.extend_from_slice(&bytes[start..start + 2]);
157 path.push(b'/');
158 }
159 path.extend_from_slice(&bytes[split * 2..]);
160 path
161}
162
163fn write_notes_subtree(repo: &Repository, entries: &[NotesTreeEntry]) -> Result<ObjectId> {
164 let mut children: BTreeMap<Vec<u8>, NotesTreeChild> = BTreeMap::new();
165
166 for entry in entries {
167 if let Some(slash_pos) = entry.path.iter().position(|byte| *byte == b'/') {
168 let child_name = entry.path[..slash_pos].to_vec();
169 let child_entry = NotesTreeEntry {
170 mode: entry.mode,
171 path: entry.path[slash_pos + 1..].to_vec(),
172 oid: entry.oid,
173 };
174 children
175 .entry(child_name)
176 .or_insert_with(|| NotesTreeChild::Tree(Vec::new()));
177 if let Some(NotesTreeChild::Tree(tree_entries)) =
178 children.get_mut(&entry.path[..slash_pos])
179 {
180 tree_entries.push(child_entry);
181 }
182 } else {
183 children.insert(
184 entry.path.clone(),
185 NotesTreeChild::Blob {
186 mode: entry.mode,
187 oid: entry.oid,
188 },
189 );
190 }
191 }
192
193 let mut tree_entries = Vec::with_capacity(children.len());
194 for (name, child) in children {
195 match child {
196 NotesTreeChild::Blob { mode, oid } => tree_entries.push(TreeEntry { mode, name, oid }),
197 NotesTreeChild::Tree(child_entries) => {
198 let oid = write_notes_subtree(repo, &child_entries)?;
199 tree_entries.push(TreeEntry {
200 mode: 0o040000,
201 name,
202 oid,
203 });
204 }
205 }
206 }
207
208 tree_entries
209 .sort_by(|a, b| tree_entry_cmp(&a.name, a.mode == 0o040000, &b.name, b.mode == 0o040000));
210
211 let tree_data = serialize_tree(&tree_entries);
212 repo.odb
213 .write(ObjectKind::Tree, &tree_data)
214 .map_err(Into::into)
215}
216
217pub fn write_notes_commit(
219 repo: &Repository,
220 notes_ref: &str,
221 entries: &[NotesTreeEntry],
222 message: &str,
223) -> Result<()> {
224 let fanout = notes_fanout(entries);
225 let rewritten_entries: Vec<_> = entries
226 .iter()
227 .map(|entry| NotesTreeEntry {
228 mode: entry.mode,
229 path: note_object_name(&entry.path)
230 .map(|name| path_with_fanout(&name, fanout))
231 .unwrap_or_else(|| entry.path.clone()),
232 oid: entry.oid,
233 })
234 .collect();
235 let tree_oid = write_notes_subtree(repo, &rewritten_entries)?;
236
237 let parent = resolve_ref(&repo.git_dir, notes_ref).ok();
239
240 let config = ConfigSet::load(Some(&repo.git_dir), true)?;
242 let now = OffsetDateTime::now_utc();
243 let author = build_ident_role(&config, now, "AUTHOR");
244 let committer = build_ident_role(&config, now, "COMMITTER");
245
246 let commit = CommitData {
247 tree: tree_oid,
248 parents: parent.into_iter().collect(),
249 author,
250 committer: committer.clone(),
251 author_raw: Vec::new(),
252 committer_raw: Vec::new(),
253 encoding: None,
254 message: if message.ends_with('\n') {
255 message.to_owned()
256 } else {
257 format!("{message}\n")
258 },
259 raw_message: None,
260 };
261
262 let commit_data = serialize_commit(&commit);
263 let commit_oid = repo.odb.write(ObjectKind::Commit, &commit_data)?;
264
265 let old_oid = resolve_ref(&repo.git_dir, notes_ref).unwrap_or_else(|_| zero_oid());
266 write_ref(&repo.git_dir, notes_ref, &commit_oid)?;
267 if should_autocreate_reflog(&repo.git_dir, notes_ref) {
268 let msg = message.trim_end_matches('\n');
269 let reflog_msg = format!("notes: {msg}");
270 let _ = append_reflog(
271 &repo.git_dir,
272 notes_ref,
273 &old_oid,
274 &commit_oid,
275 &committer,
276 &reflog_msg,
277 false,
278 );
279 }
280 Ok(())
281}
282
283fn build_ident_role(config: &ConfigSet, now: OffsetDateTime, prefix: &str) -> String {
288 let name_key = format!("GIT_{prefix}_NAME");
289 let email_key = format!("GIT_{prefix}_EMAIL");
290 let date_key = format!("GIT_{prefix}_DATE");
291
292 let name = std::env::var(&name_key)
293 .ok()
294 .filter(|n| !n.trim().is_empty())
295 .or_else(|| {
296 if prefix == "COMMITTER" {
297 std::env::var("GIT_AUTHOR_NAME")
298 .ok()
299 .filter(|n| !n.trim().is_empty())
300 } else {
301 None
302 }
303 })
304 .or_else(|| config.get("user.name"))
305 .unwrap_or_else(|| "Unknown".to_owned());
306
307 let email = std::env::var(&email_key)
308 .ok()
309 .filter(|e| !e.trim().is_empty())
310 .or_else(|| {
311 if prefix == "COMMITTER" {
312 std::env::var("GIT_AUTHOR_EMAIL")
313 .ok()
314 .filter(|e| !e.trim().is_empty())
315 } else {
316 None
317 }
318 })
319 .or_else(|| config.get("user.email"))
320 .unwrap_or_default();
321
322 let date = std::env::var(&date_key)
323 .ok()
324 .filter(|d| !d.trim().is_empty())
325 .and_then(|d| crate::commit::parse_date_to_git_timestamp(&d).or(Some(d)))
326 .unwrap_or_else(|| {
327 let epoch = now.unix_timestamp();
328 let offset = now.offset();
329 let hours = offset.whole_hours();
330 let minutes = offset.minutes_past_hour().unsigned_abs();
331 format!("{epoch} {hours:+03}{minutes:02}")
332 });
333
334 format!("{name} <{email}> {date}")
335}
336
337pub fn notes_merge_git_dir(repo: &Repository) -> std::path::PathBuf {
339 repo.git_dir.clone()
340}
341
342#[derive(Clone, Copy, Debug, PartialEq, Eq)]
343pub enum NotesMergeStrategy {
344 Manual,
345 Ours,
346 Theirs,
347 Union,
348 CatSortUniq,
349}
350
351#[derive(Clone, Debug)]
352enum LocalNoteState {
353 Unset,
354 Deleted,
355 Present(ObjectId),
356}
357
358#[derive(Clone, Debug)]
359struct NotesMergePair {
360 obj: ObjectId,
361 base_blob: Option<ObjectId>,
362 remote_blob: Option<ObjectId>,
363 local: LocalNoteState,
364}
365
366pub fn expand_notes_ref(short_or_full: &str) -> String {
368 if short_or_full.starts_with("refs/notes/") {
369 short_or_full.to_owned()
370 } else if short_or_full.starts_with("notes/") {
371 format!("refs/{short_or_full}")
372 } else {
373 format!("refs/notes/{short_or_full}")
374 }
375}
376
377pub fn notes_merge_worktree_path(repo: &Repository) -> std::path::PathBuf {
378 notes_merge_git_dir(repo).join(NOTES_MERGE_WORKTREE)
379}
380
381pub fn notes_merge_worktree_nonempty(worktree: &std::path::Path) -> bool {
383 if !worktree.is_dir() {
384 return false;
385 }
386 let Ok(entries) = fs::read_dir(worktree) else {
387 return false;
388 };
389 entries.flatten().next().is_some()
390}
391
392pub fn parse_notes_merge_strategy_value(s: &str) -> Option<NotesMergeStrategy> {
393 match s {
394 "manual" => Some(NotesMergeStrategy::Manual),
395 "ours" => Some(NotesMergeStrategy::Ours),
396 "theirs" => Some(NotesMergeStrategy::Theirs),
397 "union" => Some(NotesMergeStrategy::Union),
398 "cat_sort_uniq" => Some(NotesMergeStrategy::CatSortUniq),
399 _ => None,
400 }
401}
402
403fn notes_tree_blob_by_object(
405 repo: &Repository,
406 tree_oid: &ObjectId,
407) -> Result<HashMap<ObjectId, ObjectId>> {
408 let mut flat = Vec::new();
409 collect_notes_tree_entries(repo, tree_oid, b"", &mut flat)?;
410 let mut map = HashMap::new();
411 for entry in flat {
412 let Some(hex) = note_object_name(&entry.path) else {
413 continue;
414 };
415 let obj = ObjectId::from_hex(&hex)
416 .map_err(|e| Error::Message(format!("invalid note object id in tree: {e}")))?;
417 map.insert(obj, entry.oid);
418 }
419 Ok(map)
420}
421
422fn diff_note_blob_changes(
423 repo: &Repository,
424 old_tree: Option<&ObjectId>,
425 new_tree: Option<&ObjectId>,
426) -> Result<Vec<(ObjectId, Option<ObjectId>, Option<ObjectId>)>> {
427 let old_map = match old_tree {
428 Some(t) => notes_tree_blob_by_object(repo, t)?,
429 None => HashMap::new(),
430 };
431 let new_map = match new_tree {
432 Some(t) => notes_tree_blob_by_object(repo, t)?,
433 None => HashMap::new(),
434 };
435 let keys: BTreeSet<ObjectId> = old_map.keys().chain(new_map.keys()).copied().collect();
436 let mut out = Vec::new();
437 for obj in keys {
438 let o_old = old_map.get(&obj).copied();
439 let o_new = new_map.get(&obj).copied();
440 match (o_old, o_new) {
441 (None, Some(new_b)) => out.push((obj, None, Some(new_b))),
442 (Some(old_b), None) => out.push((obj, Some(old_b), None)),
443 (Some(old_b), Some(new_b)) if old_b != new_b => {
444 out.push((obj, Some(old_b), Some(new_b)));
445 }
446 _ => {}
447 }
448 }
449 Ok(out)
450}
451
452fn build_merge_pairs(
453 base_tree: Option<ObjectId>,
454 local_tree: ObjectId,
455 remote_tree: ObjectId,
456 repo: &Repository,
457) -> Result<Vec<NotesMergePair>> {
458 let remote_raw = diff_note_blob_changes(repo, base_tree.as_ref(), Some(&remote_tree))?;
459 let local_raw = diff_note_blob_changes(repo, base_tree.as_ref(), Some(&local_tree))?;
460 let mut map: HashMap<ObjectId, NotesMergePair> = HashMap::new();
461 for (obj, old_b, new_b) in remote_raw {
462 map.insert(
463 obj,
464 NotesMergePair {
465 obj,
466 base_blob: old_b,
467 remote_blob: new_b,
468 local: LocalNoteState::Unset,
469 },
470 );
471 }
472
473 for (obj, old_b, new_b) in local_raw {
474 let local_state = match new_b {
475 Some(new_oid) => LocalNoteState::Present(new_oid),
476 None => LocalNoteState::Deleted,
477 };
478 if let Some(p) = map.get_mut(&obj) {
479 p.local = local_state;
480 } else {
481 map.insert(
482 obj,
483 NotesMergePair {
484 obj,
485 base_blob: old_b,
486 remote_blob: old_b,
487 local: local_state,
488 },
489 );
490 }
491 }
492
493 let mut v: Vec<_> = map.into_values().collect();
494 v.sort_by(|a, b| a.obj.cmp(&b.obj));
495 Ok(v)
496}
497
498fn read_blob_bytes(repo: &Repository, oid: &ObjectId) -> Result<Vec<u8>> {
499 let obj = repo.odb.read(oid)?;
500 if obj.kind != ObjectKind::Blob {
501 return Err(Error::Message("expected blob for note".into()));
502 }
503 Ok(obj.data)
504}
505
506pub fn combine_notes_concatenate(
508 repo: &Repository,
509 cur: Option<&ObjectId>,
510 new_oid: Option<&ObjectId>,
511) -> Result<ObjectId> {
512 let new_data = match new_oid {
513 Some(n) => {
514 let obj = repo.odb.read(n)?;
515 if obj.kind != ObjectKind::Blob || obj.data.is_empty() {
516 Vec::new()
517 } else {
518 obj.data
519 }
520 }
521 None => Vec::new(),
522 };
523 if new_data.is_empty() {
524 let Some(c) = cur else {
525 return Err(Error::Message(
526 "combine_notes_concatenate: empty new and no current".into(),
527 ));
528 };
529 return Ok(*c);
530 }
531
532 let cur_data = match cur {
533 Some(c) => {
534 let obj = repo.odb.read(c)?;
535 if obj.kind != ObjectKind::Blob || obj.data.is_empty() {
536 Vec::new()
537 } else {
538 obj.data
539 }
540 }
541 None => Vec::new(),
542 };
543
544 if cur_data.is_empty() {
545 return Ok(repo.odb.write(ObjectKind::Blob, &new_data)?);
546 }
547
548 let mut cur_len = cur_data.len();
549 if cur_len > 0 && cur_data[cur_len - 1] == b'\n' {
550 cur_len -= 1;
551 }
552 let mut buf = Vec::with_capacity(cur_len + 2 + new_data.len());
553 buf.extend_from_slice(&cur_data[..cur_len]);
554 buf.push(b'\n');
555 buf.push(b'\n');
556 buf.extend_from_slice(&new_data);
557 Ok(repo.odb.write(ObjectKind::Blob, &buf)?)
558}
559
560fn note_blob_lines(data: &[u8]) -> Vec<String> {
561 if data.is_empty() {
562 return Vec::new();
563 }
564 let s = String::from_utf8_lossy(data);
565 s.split('\n').map(|l| l.to_owned()).collect()
566}
567
568pub fn combine_notes_cat_sort_uniq(
570 repo: &Repository,
571 cur: Option<&ObjectId>,
572 new_oid: Option<&ObjectId>,
573) -> Result<ObjectId> {
574 let mut lines: Vec<String> = Vec::new();
575 for oid in [cur, new_oid].into_iter().flatten() {
576 let obj = repo.odb.read(oid)?;
577 if obj.kind == ObjectKind::Blob && !obj.data.is_empty() {
578 lines.extend(note_blob_lines(&obj.data));
579 }
580 }
581 lines.retain(|l| !l.is_empty());
582 lines.sort();
583 lines.dedup();
584 let mut buf = String::new();
585 for l in &lines {
586 buf.push_str(l);
587 buf.push('\n');
588 }
589 Ok(repo.odb.write(ObjectKind::Blob, buf.as_bytes())?)
590}
591
592fn blob_to_lines(data: &[u8]) -> Vec<String> {
593 if data.is_empty() {
594 return vec![String::new()];
595 }
596 let s = String::from_utf8_lossy(data).into_owned();
597 s.split_inclusive('\n').map(|l| l.to_owned()).collect()
598}
599
600fn merge_note_blobs_conflict_markers(
601 repo: &Repository,
602 base: Option<&ObjectId>,
603 local: &ObjectId,
604 remote: &ObjectId,
605 local_ref: &str,
606 remote_ref: &str,
607) -> Result<Vec<u8>> {
608 let base_lines: Vec<String> = match base {
609 Some(b) => blob_to_lines(&read_blob_bytes(repo, b)?),
610 None => vec![String::new()],
611 };
612 let local_lines = blob_to_lines(&read_blob_bytes(repo, local)?);
613 let remote_lines = blob_to_lines(&read_blob_bytes(repo, remote)?);
614
615 let base_refs: Vec<&str> = base_lines.iter().map(|s| s.as_str()).collect();
616 let local_refs: Vec<&str> = local_lines.iter().map(|s| s.as_str()).collect();
617 let remote_refs: Vec<&str> = remote_lines.iter().map(|s| s.as_str()).collect();
618 let m3 = Merge3::new(&base_refs, &local_refs, &remote_refs);
619 let markers = StandardMarkers::new(Some(local_ref), Some(remote_ref));
620 let merged: String = m3
621 .merge_lines(false, &markers)
622 .into_iter()
623 .map(|cow| cow.into_owned())
624 .collect();
625 Ok(merged.into_bytes())
626}
627
628fn write_note_conflict_file(
629 path: &std::path::Path,
630 repo: &Repository,
631 pair: &NotesMergePair,
632 local_ref: &str,
633 remote_ref: &str,
634) -> Result<()> {
635 let data = match (&pair.local, &pair.remote_blob) {
636 (LocalNoteState::Deleted, Some(r)) => read_blob_bytes(repo, r)?,
637 (LocalNoteState::Present(l), None) => read_blob_bytes(repo, l)?,
638 (LocalNoteState::Present(l), Some(r)) => merge_note_blobs_conflict_markers(
639 repo,
640 pair.base_blob.as_ref(),
641 l,
642 r,
643 local_ref,
644 remote_ref,
645 )?,
646 _ => {
647 return Err(Error::Message(
648 "unexpected notes merge conflict shape".into(),
649 ))
650 }
651 };
652 fs::write(path, data)?;
653 Ok(())
654}
655
656fn merge_one_note_change(
657 repo: &Repository,
658 pair: &NotesMergePair,
659 strategy: NotesMergeStrategy,
660 local_ref: &str,
661 remote_ref: &str,
662 worktree: &std::path::Path,
663 commit_msg: &mut String,
664 entries: &mut Vec<NotesTreeEntry>,
665 has_worktree: &mut bool,
666) -> Result<bool> {
667 let obj_hex = pair.obj.to_hex();
668 let path = worktree.join(&obj_hex);
669 match strategy {
670 NotesMergeStrategy::Manual => {
671 if !*has_worktree && notes_merge_worktree_nonempty(worktree) {
672 return Err(Error::Message(
673 "You have not concluded your previous notes merge (.git/NOTES_MERGE_* exists).\n\
674Please, use 'git notes merge --commit' or 'git notes merge --abort' to commit/abort the \
675previous merge before you start a new notes merge."
676 .into(),
677 ));
678 }
679 if !commit_msg.contains("Conflicts:") {
680 commit_msg.push_str("\n\nConflicts:\n");
681 }
682 commit_msg.push_str(&format!("\t{obj_hex}\n"));
683 if !*has_worktree {
684 let test = worktree.join(".test");
685 fs::create_dir_all(worktree)?;
686 fs::write(&test, b"")?;
687 let _ = fs::remove_file(test);
688 *has_worktree = true;
689 }
690 write_note_conflict_file(&path, repo, pair, local_ref, remote_ref)?;
691 entries.retain(|e| note_object_name(&e.path).as_deref() != Some(obj_hex.as_str()));
692 Ok(true)
693 }
694 NotesMergeStrategy::Ours => Ok(false),
695 NotesMergeStrategy::Theirs => {
696 if let Some(r) = pair.remote_blob {
697 upsert_note_entry(entries, &obj_hex, r);
698 } else {
699 entries.retain(|e| note_object_name(&e.path).as_deref() != Some(obj_hex.as_str()));
700 }
701 Ok(false)
702 }
703 NotesMergeStrategy::Union => {
704 match (&pair.local, &pair.remote_blob) {
705 (LocalNoteState::Deleted, None) => {
706 entries
707 .retain(|e| note_object_name(&e.path).as_deref() != Some(obj_hex.as_str()));
708 }
709 (LocalNoteState::Deleted, Some(r)) => {
710 let out = combine_notes_concatenate(repo, None, Some(r))?;
711 upsert_note_entry(entries, &obj_hex, out);
712 }
713 (LocalNoteState::Present(_), None) => {}
714 (LocalNoteState::Present(l), Some(r)) => {
715 let out = combine_notes_concatenate(repo, Some(l), Some(r))?;
716 upsert_note_entry(entries, &obj_hex, out);
717 }
718 (LocalNoteState::Unset, _) => {
719 return Err(Error::Message(
720 "unexpected notes merge pair: local unset in union strategy".into(),
721 ));
722 }
723 }
724 Ok(false)
725 }
726 NotesMergeStrategy::CatSortUniq => {
727 match (&pair.local, &pair.remote_blob) {
728 (LocalNoteState::Deleted, None) => {
729 entries
730 .retain(|e| note_object_name(&e.path).as_deref() != Some(obj_hex.as_str()));
731 }
732 (LocalNoteState::Deleted, Some(r)) => {
733 let out = combine_notes_cat_sort_uniq(repo, None, Some(r))?;
734 upsert_note_entry(entries, &obj_hex, out);
735 }
736 (LocalNoteState::Present(_), None) => {}
737 (LocalNoteState::Present(l), Some(r)) => {
738 let out = combine_notes_cat_sort_uniq(repo, Some(l), Some(r))?;
739 upsert_note_entry(entries, &obj_hex, out);
740 }
741 (LocalNoteState::Unset, _) => {
742 return Err(Error::Message(
743 "unexpected notes merge pair: local unset in cat_sort_uniq strategy".into(),
744 ));
745 }
746 }
747 Ok(false)
748 }
749 }
750}
751
752pub fn upsert_note_entry(entries: &mut Vec<NotesTreeEntry>, hex: &str, blob: ObjectId) {
753 entries.retain(|e| note_object_name(&e.path).as_deref() != Some(hex));
754 entries.push(NotesTreeEntry {
755 mode: 0o100644,
756 path: hex.as_bytes().to_vec(),
757 oid: blob,
758 });
759}
760
761fn remote_unchanged(base: Option<ObjectId>, remote: Option<ObjectId>) -> bool {
762 match (base, remote) {
763 (Some(b), Some(r)) => b == r,
764 (None, None) => true,
765 _ => false,
766 }
767}
768
769fn same_change_local_remote(p: &NotesMergePair) -> bool {
770 match (&p.local, p.remote_blob) {
771 (LocalNoteState::Present(l), Some(r)) => l == &r,
772 (LocalNoteState::Deleted, None) => true,
773 (LocalNoteState::Unset, Some(r)) => Some(r) == p.base_blob,
774 (LocalNoteState::Unset, None) => p.base_blob.is_none(),
775 _ => false,
776 }
777}
778
779fn no_local_change(local: &LocalNoteState, base: Option<ObjectId>) -> bool {
780 match local {
781 LocalNoteState::Unset => true,
782 LocalNoteState::Present(l) => Some(*l) == base,
783 LocalNoteState::Deleted => false,
784 }
785}
786
787fn adopt_remote_note(entries: &mut Vec<NotesTreeEntry>, obj_hex: &str, remote: Option<ObjectId>) {
788 match remote {
789 Some(oid) => upsert_note_entry(entries, obj_hex, oid),
790 None => entries.retain(|e| note_object_name(&e.path).as_deref() != Some(obj_hex)),
791 }
792}
793
794fn merge_changes_into_entries(
795 repo: &Repository,
796 pairs: &[NotesMergePair],
797 strategy: NotesMergeStrategy,
798 local_ref: &str,
799 remote_ref: &str,
800 worktree: &std::path::Path,
801 commit_msg: &mut String,
802 entries: &mut Vec<NotesTreeEntry>,
803) -> Result<usize> {
804 let mut conflicts = 0usize;
805 let mut has_worktree = false;
806 for p in pairs {
807 if remote_unchanged(p.base_blob, p.remote_blob) {
808 continue;
809 }
810 if same_change_local_remote(p) {
811 continue;
812 }
813 if no_local_change(&p.local, p.base_blob) {
814 adopt_remote_note(entries, &p.obj.to_hex(), p.remote_blob);
815 continue;
816 }
817 if merge_one_note_change(
818 repo,
819 p,
820 strategy,
821 local_ref,
822 remote_ref,
823 worktree,
824 commit_msg,
825 entries,
826 &mut has_worktree,
827 )? {
828 conflicts += 1;
829 }
830 }
831 Ok(conflicts)
832}
833
834fn resolve_commit_tree(repo: &Repository, commit_oid: &ObjectId) -> Result<ObjectId> {
835 let obj = repo.odb.read(commit_oid)?;
836 if obj.kind != ObjectKind::Commit {
837 return Err(Error::Message("expected commit".into()));
838 }
839 Ok(parse_commit(&obj.data)?.tree)
840}
841
842fn resolve_notes_commit_optional(repo: &Repository, notes_ref: &str) -> Result<Option<ObjectId>> {
843 let oid = match resolve_ref(&repo.git_dir, notes_ref) {
844 Ok(o) => o,
845 Err(_) => return Ok(None),
846 };
847 let obj = repo.odb.read(&oid)?;
848 if obj.kind != ObjectKind::Commit {
849 return Err(Error::Message(format!(
850 "{notes_ref} does not point to a commit"
851 )));
852 }
853 Ok(Some(oid))
854}
855
856pub fn write_notes_commit_with_parents(
857 repo: &Repository,
858 _notes_ref: &str,
859 entries: &[NotesTreeEntry],
860 message: &str,
861 parents: &[ObjectId],
862) -> Result<ObjectId> {
863 let fanout = notes_fanout(entries);
864 let rewritten_entries: Vec<_> = entries
865 .iter()
866 .map(|entry| NotesTreeEntry {
867 mode: entry.mode,
868 path: note_object_name(&entry.path)
869 .map(|name| path_with_fanout(&name, fanout))
870 .unwrap_or_else(|| entry.path.clone()),
871 oid: entry.oid,
872 })
873 .collect();
874 let tree_oid = write_notes_subtree(repo, &rewritten_entries)?;
875 let config = ConfigSet::load(Some(&repo.git_dir), true)?;
876 let now = OffsetDateTime::now_utc();
877 let author = build_ident_role(&config, now, "AUTHOR");
878 let committer = build_ident_role(&config, now, "COMMITTER");
879 let commit = CommitData {
880 tree: tree_oid,
881 parents: parents.to_vec(),
882 author,
883 committer,
884 author_raw: Vec::new(),
885 committer_raw: Vec::new(),
886 encoding: None,
887 message: if message.ends_with('\n') {
888 message.to_owned()
889 } else {
890 format!("{message}\n")
891 },
892 raw_message: None,
893 };
894 let commit_data = serialize_commit(&commit);
895 Ok(repo.odb.write(ObjectKind::Commit, &commit_data)?)
896}
897
898pub fn notes_merge_inner(
899 repo: &Repository,
900 local_ref: &str,
901 remote_ref: &str,
902 strategy: NotesMergeStrategy,
903) -> Result<std::result::Result<ObjectId, ObjectId>> {
904 let local_commit = resolve_notes_commit_optional(repo, local_ref)?;
905 let remote_commit = resolve_notes_commit_optional(repo, remote_ref)?;
906 match (local_commit, remote_commit) {
907 (None, None) => {
908 return Err(Error::Message(format!(
909 "Cannot merge empty notes ref ({remote_ref}) into empty notes ref ({local_ref})"
910 )))
911 }
912 (None, Some(r)) => Ok(Ok(r)),
913 (Some(l), None) => Ok(Ok(l)),
914 (Some(local_oid), Some(remote_oid)) => {
915 if local_oid == remote_oid {
916 return Ok(Ok(local_oid));
917 }
918 let bases = merge_bases_first_vs_rest(repo, local_oid, &[remote_oid])?;
919 let base_commit = bases.into_iter().next();
920 if Some(local_oid) == base_commit {
921 return Ok(Ok(remote_oid));
922 }
923 if Some(remote_oid) == base_commit {
924 return Ok(Ok(local_oid));
925 }
926 let base_tree = base_commit
927 .map(|bc| resolve_commit_tree(repo, &bc))
928 .transpose()?;
929 let local_tree = resolve_commit_tree(repo, &local_oid)?;
930 let remote_tree = resolve_commit_tree(repo, &remote_oid)?;
931 let mut commit_msg = format!("Merged notes from {remote_ref} into {local_ref}\n\n");
932 let pairs = build_merge_pairs(base_tree, local_tree, remote_tree, repo)?;
933 let mut entries = read_notes_tree(repo, local_ref)?;
934 let worktree = notes_merge_worktree_path(repo);
935 let conflicts = merge_changes_into_entries(
936 repo,
937 &pairs,
938 strategy,
939 local_ref,
940 remote_ref,
941 &worktree,
942 &mut commit_msg,
943 &mut entries,
944 )?;
945 let merge_parents = vec![local_oid, remote_oid];
946 if conflicts > 0 {
947 let partial = write_notes_commit_with_parents(
948 repo,
949 local_ref,
950 &entries,
951 &commit_msg,
952 &merge_parents,
953 )?;
954 return Ok(Err(partial));
955 }
956 let new_oid = write_notes_commit_with_parents(
957 repo,
958 local_ref,
959 &entries,
960 &commit_msg,
961 &merge_parents,
962 )?;
963 Ok(Ok(new_oid))
964 }
965 }
966}