1use std::collections::{BTreeMap, BinaryHeap, HashMap, HashSet};
37
38use gix_hash::{Kind as HashKind, ObjectId};
39use gunnar_store::git_storage_trait::{
40 self as gst, Caps, GcReport, GitOps, GitServe, ObjType, PackStats, ReachSet, RefCas, RefRow,
41 RefUpdate, Stored, TxId,
42};
43use gunnar_store::object::{self, TreeIter};
44
45pub const MAX_TEXT_MERGE_BYTES: usize = 16 * 1024 * 1024;
48
49pub const MAX_OBJECT_READS: usize = 500_000;
53
54const IFMT: u32 = 0o170000;
56const IFDIR: u32 = 0o040000;
57const IFREG: u32 = 0o100000;
58
59#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct EngineError(pub String);
63
64impl std::fmt::Display for EngineError {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 f.write_str(&self.0)
67 }
68}
69
70type Result<T> = std::result::Result<T, EngineError>;
71
72fn err(what: impl Into<String>) -> EngineError {
73 EngineError(what.into())
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct Conflict {
79 pub path: String,
81 pub why: &'static str,
84}
85
86impl std::fmt::Display for Conflict {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 write!(f, "{} ({})", self.path, self.why)
89 }
90}
91
92#[derive(Debug, Default)]
96pub struct NewObjects {
97 order: Vec<ObjectId>,
98 objects: HashMap<Vec<u8>, (ObjType, Vec<u8>)>,
99}
100
101impl NewObjects {
102 fn add(&mut self, hash: HashKind, kind: ObjType, content: Vec<u8>) -> Result<ObjectId> {
103 let id = crate::first_commit::object_id(hash, word(kind), &content).map_err(err)?;
104 if !self.objects.contains_key(id.as_bytes()) {
105 self.order.push(id);
106 self.objects.insert(id.as_bytes().to_vec(), (kind, content));
107 }
108 Ok(id)
109 }
110
111 pub fn get(&self, id: &[u8]) -> Option<&(ObjType, Vec<u8>)> {
113 self.objects.get(id)
114 }
115
116 pub fn len(&self) -> usize {
118 self.order.len()
119 }
120
121 pub fn is_empty(&self) -> bool {
123 self.order.is_empty()
124 }
125
126 pub fn pack(&self, hash: HashKind, git: &dyn GitServe) -> Result<Option<Vec<u8>>> {
129 let mut items: Vec<(u8, &[u8])> = Vec::new();
130 for id in &self.order {
131 if git
132 .has(id.as_bytes())
133 .map_err(|e| err(format!("asking the store for {id}: {e:#}")))?
134 {
135 continue;
136 }
137 let (kind, content) = &self.objects[id.as_bytes()];
138 items.push((kind.code(), content));
139 }
140 if items.is_empty() {
141 return Ok(None);
142 }
143 crate::first_commit::pack_of(hash, &items)
144 .map(Some)
145 .map_err(err)
146 }
147}
148
149fn word(kind: ObjType) -> &'static str {
150 match kind {
151 ObjType::Commit => "commit",
152 ObjType::Tree => "tree",
153 ObjType::Tag => "tag",
154 _ => "blob",
155 }
156}
157
158fn hex(id: &ObjectId) -> String {
159 id.to_hex().to_string()
160}
161
162fn read_as(git: &dyn GitServe, id: &ObjectId, want: ObjType) -> Result<Vec<u8>> {
164 match git.read(id.as_bytes()) {
165 Ok(Some((kind, bytes))) if kind == want => Ok(bytes),
166 Ok(Some((kind, _))) => Err(err(format!(
167 "{} is a {kind:?}, not the {want:?} the merge expected",
168 hex(id)
169 ))),
170 Ok(None) => Err(err(format!("{} is not in the repository", hex(id)))),
171 Err(e) => Err(err(format!("reading {}: {e:#}", hex(id)))),
172 }
173}
174
175fn commit_facts(git: &dyn GitServe, hash: HashKind, id: &ObjectId) -> Result<(Vec<ObjectId>, i64)> {
177 let bytes = read_as(git, id, ObjType::Commit)?;
178 let mut parents = Vec::new();
179 let (_, at) = object::commit_facts(&bytes, hash, &mut parents)
180 .map_err(|e| err(format!("decoding commit {}: {e}", hex(id))))?;
181 Ok((parents, at))
182}
183
184pub fn commit_tree(git: &dyn GitServe, hash: HashKind, id: &ObjectId) -> Result<ObjectId> {
186 let bytes = read_as(git, id, ObjType::Commit)?;
187 let mut parents = Vec::new();
188 object::commit_links(&bytes, hash, &mut parents)
189 .map_err(|e| err(format!("decoding commit {}: {e}", hex(id))))
190}
191
192const P1: u8 = 1;
195const P2: u8 = 2;
196const STALE: u8 = 4;
197const RESULT: u8 = 8;
198
199pub fn merge_base(
212 git: &dyn GitServe,
213 hash: HashKind,
214 a: &ObjectId,
215 b: &ObjectId,
216 budget: usize,
217) -> Result<ObjectId> {
218 if a == b {
219 return Ok(*a);
220 }
221 let mut flags: HashMap<ObjectId, u8> = HashMap::new();
222 let mut queue: BinaryHeap<(i64, ObjectId)> = BinaryHeap::new();
223 let mut facts: HashMap<ObjectId, (Vec<ObjectId>, i64)> = HashMap::new();
224 let load =
225 |id: &ObjectId, facts: &mut HashMap<ObjectId, (Vec<ObjectId>, i64)>| -> Result<i64> {
226 if let Some((_, at)) = facts.get(id) {
227 return Ok(*at);
228 }
229 let f = commit_facts(git, hash, id)?;
230 let at = f.1;
231 facts.insert(*id, f);
232 Ok(at)
233 };
234 for (id, paint) in [(a, P1), (b, P2)] {
235 let at = load(id, &mut facts)?;
236 *flags.entry(*id).or_default() |= paint;
237 queue.push((at, *id));
238 }
239 let mut results: Vec<ObjectId> = Vec::new();
240 let mut walked = 0usize;
241 while queue
242 .iter()
243 .any(|(_, id)| flags.get(id).copied().unwrap_or(0) & STALE == 0)
244 {
245 let Some((_, id)) = queue.pop() else { break };
246 walked += 1;
247 if walked > budget {
248 return Err(err(format!(
249 "the merge base of {} and {} is more than {budget} commits away; refusing \
250 rather than walking further",
251 hex(a),
252 hex(b)
253 )));
254 }
255 let mut paint = flags[&id] & (P1 | P2 | STALE);
256 if paint == P1 | P2 {
257 if flags[&id] & RESULT == 0 {
258 *flags.get_mut(&id).expect("painted") |= RESULT;
259 results.push(id);
260 }
261 paint |= STALE;
262 }
263 load(&id, &mut facts)?;
264 let parents = facts[&id].0.clone();
265 for parent in parents {
266 let have = flags.get(&parent).copied().unwrap_or(0);
267 if have & paint == paint {
268 continue;
269 }
270 let at = load(&parent, &mut facts)?;
271 *flags.entry(parent).or_default() |= paint;
272 queue.push((at, parent));
273 }
274 }
275 results.retain(|id| flags[id] & STALE == 0);
277 let mut best: Vec<ObjectId> = Vec::new();
278 for candidate in &results {
279 let redundant = results.iter().any(|other| {
280 other != candidate
281 && gunnar_policy::ancestry::is_descendant_of(git, other, candidate, budget)
282 .unwrap_or(false)
283 });
284 if !redundant {
285 best.push(*candidate);
286 }
287 }
288 match best.as_slice() {
289 [] => Err(err(format!(
290 "{} and {} share no history: there is no merge base to merge from",
291 hex(a),
292 hex(b)
293 ))),
294 [one] => Ok(*one),
295 many => Err(err(format!(
296 "{} and {} have {} best merge bases ({}): a criss-cross history, which gunnar \
297 does not merge on the server — merge locally and push the result",
298 hex(a),
299 hex(b),
300 many.len(),
301 many.iter()
302 .map(|id| hex(id)[..12].to_owned())
303 .collect::<Vec<_>>()
304 .join(", ")
305 ))),
306 }
307}
308
309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
313struct Ent {
314 mode: u32,
315 oid: ObjectId,
316}
317
318impl Ent {
319 fn is_tree(&self) -> bool {
320 self.mode & IFMT == IFDIR
321 }
322 fn is_regular(&self) -> bool {
323 self.mode & IFMT == IFREG
324 }
325}
326
327#[derive(Debug)]
329pub enum TreeMerge {
330 Clean(ObjectId),
332 Conflicts(Vec<Conflict>),
334}
335
336pub struct Merger<'a> {
338 git: &'a dyn GitServe,
339 hash: HashKind,
340 encrypted: bool,
343 pub new: NewObjects,
345 conflicts: Vec<Conflict>,
346 reads: usize,
347}
348
349impl<'a> Merger<'a> {
350 pub fn new(git: &'a dyn GitServe, hash: HashKind, encrypted: bool) -> Self {
352 Merger {
353 git,
354 hash,
355 encrypted,
356 new: NewObjects::default(),
357 conflicts: Vec::new(),
358 reads: 0,
359 }
360 }
361
362 fn read(&mut self, id: &ObjectId, want: ObjType) -> Result<Vec<u8>> {
363 self.reads += 1;
364 if self.reads > MAX_OBJECT_READS {
365 return Err(err(format!(
366 "the merge read more than {MAX_OBJECT_READS} objects; refusing rather than \
367 reading further"
368 )));
369 }
370 read_as(self.git, id, want)
371 }
372
373 fn entries(&mut self, tree: Option<&ObjectId>) -> Result<BTreeMap<Vec<u8>, Ent>> {
374 let mut out = BTreeMap::new();
375 let Some(tree) = tree else { return Ok(out) };
376 let bytes = self.read(tree, ObjType::Tree)?;
377 for entry in TreeIter::from_bytes(&bytes, self.hash).named() {
378 let entry = entry.map_err(|e| err(format!("decoding tree {}: {e}", hex(tree))))?;
379 out.insert(
380 entry.name.to_vec(),
381 Ent {
382 mode: entry.mode,
383 oid: entry.oid,
384 },
385 );
386 }
387 Ok(out)
388 }
389
390 fn write_tree(&mut self, entries: &BTreeMap<Vec<u8>, Ent>) -> Result<ObjectId> {
393 let mut sorted: Vec<(Vec<u8>, &Ent)> = entries
394 .iter()
395 .map(|(name, ent)| {
396 let mut key = name.clone();
397 if ent.is_tree() {
398 key.push(b'/');
399 }
400 (key, ent)
401 })
402 .collect();
403 sorted.sort_by(|a, b| a.0.cmp(&b.0));
404 let mut body = Vec::new();
405 for (key, ent) in sorted {
406 let name = if ent.is_tree() {
407 &key[..key.len() - 1]
408 } else {
409 &key[..]
410 };
411 let mode = if ent.is_tree() { IFDIR } else { ent.mode };
412 body.extend_from_slice(format!("{mode:o} ").as_bytes());
413 body.extend_from_slice(name);
414 body.push(0);
415 body.extend_from_slice(ent.oid.as_bytes());
416 }
417 self.new.add(self.hash, ObjType::Tree, body)
418 }
419
420 pub fn merge_trees(
423 &mut self,
424 base: Option<&ObjectId>,
425 ours: &ObjectId,
426 theirs: &ObjectId,
427 ) -> Result<TreeMerge> {
428 let root = self.merge_dir(base, Some(ours), Some(theirs), "")?;
429 if !self.conflicts.is_empty() {
430 return Ok(TreeMerge::Conflicts(std::mem::take(&mut self.conflicts)));
431 }
432 let root = match root {
433 Some(id) => id,
434 None => self.write_tree(&BTreeMap::new())?,
435 };
436 Ok(TreeMerge::Clean(root))
437 }
438
439 fn conflict(&mut self, path: String, why: &'static str) {
440 self.conflicts.push(Conflict { path, why });
441 }
442
443 fn merge_dir(
445 &mut self,
446 base: Option<&ObjectId>,
447 ours: Option<&ObjectId>,
448 theirs: Option<&ObjectId>,
449 prefix: &str,
450 ) -> Result<Option<ObjectId>> {
451 let b = self.entries(base)?;
452 let o = self.entries(ours)?;
453 let t = self.entries(theirs)?;
454 let names: std::collections::BTreeSet<&Vec<u8>> =
455 b.keys().chain(o.keys()).chain(t.keys()).collect();
456 let mut out: BTreeMap<Vec<u8>, Ent> = BTreeMap::new();
457 for name in names {
458 let path = if prefix.is_empty() {
459 String::from_utf8_lossy(name).into_owned()
460 } else {
461 format!("{prefix}/{}", String::from_utf8_lossy(name))
462 };
463 let (be, oe, te) = (
464 b.get(name).copied(),
465 o.get(name).copied(),
466 t.get(name).copied(),
467 );
468 if let Some(ent) = self.merge_entry(be, oe, te, &path)? {
469 out.insert(name.clone(), ent);
470 }
471 }
472 if out.is_empty() {
473 return Ok(None);
474 }
475 if ours.is_some() && out == o {
477 return Ok(ours.copied());
478 }
479 if theirs.is_some() && out == t {
480 return Ok(theirs.copied());
481 }
482 self.write_tree(&out).map(Some)
483 }
484
485 fn merge_entry(
486 &mut self,
487 b: Option<Ent>,
488 o: Option<Ent>,
489 t: Option<Ent>,
490 path: &str,
491 ) -> Result<Option<Ent>> {
492 if o == t {
494 return Ok(o);
495 }
496 if b == o {
497 return Ok(t);
498 }
499 if b == t {
500 return Ok(o);
501 }
502 match (b, o, t) {
504 (b, Some(o), Some(t)) if o.is_tree() && t.is_tree() => {
505 let base = b.filter(Ent::is_tree).map(|e| e.oid);
506 if b.is_some() && base.is_none() {
507 self.conflict(
508 path.to_owned(),
509 "a file on one side became a directory on both",
510 );
511 return Ok(Some(o));
512 }
513 let merged = self.merge_dir(base.as_ref(), Some(&o.oid), Some(&t.oid), path)?;
514 Ok(merged.map(|oid| Ent { mode: IFDIR, oid }))
515 }
516 (Some(b), Some(o), Some(t)) if b.is_regular() && o.is_regular() && t.is_regular() => {
517 let mode = if o.mode == t.mode || b.mode == t.mode {
518 o.mode
519 } else if b.mode == o.mode {
520 t.mode
521 } else {
522 self.conflict(path.to_owned(), "both sides changed the file mode");
523 return Ok(Some(o));
524 };
525 if o.oid == t.oid {
526 return Ok(Some(Ent { mode, oid: o.oid }));
527 }
528 if b.oid == o.oid {
529 return Ok(Some(Ent { mode, oid: t.oid }));
530 }
531 if b.oid == t.oid {
532 return Ok(Some(Ent { mode, oid: o.oid }));
533 }
534 if self.encrypted {
535 self.conflict(
536 path.to_owned(),
537 "both sides changed an encrypted file, which the server cannot read",
538 );
539 return Ok(Some(o));
540 }
541 let base = self.read(&b.oid, ObjType::Blob)?;
542 let ours = self.read(&o.oid, ObjType::Blob)?;
543 let theirs = self.read(&t.oid, ObjType::Blob)?;
544 match merge_text(&base, &ours, &theirs) {
545 Ok(merged) => {
546 let oid = self.new.add(self.hash, ObjType::Blob, merged)?;
547 Ok(Some(Ent { mode, oid }))
548 }
549 Err(why) => {
550 self.conflict(path.to_owned(), why.phrase());
551 Ok(Some(o))
552 }
553 }
554 }
555 (None, Some(_), Some(_)) => {
556 self.conflict(
557 path.to_owned(),
558 "added on both sides with different content",
559 );
560 Ok(o)
561 }
562 (Some(_), None, Some(_)) | (Some(_), Some(_), None) => {
563 self.conflict(path.to_owned(), "deleted on one side, changed on the other");
564 Ok(o)
565 }
566 (_, Some(o), Some(t)) if o.is_tree() != t.is_tree() => {
567 self.conflict(
568 path.to_owned(),
569 "a file on one side, a directory on the other",
570 );
571 Ok(Some(o))
572 }
573 _ => {
574 self.conflict(
575 path.to_owned(),
576 "both sides changed it, and it is not a regular file (a symlink, a \
577 submodule or a type change)",
578 );
579 Ok(o)
580 }
581 }
582 }
583}
584
585#[derive(Debug, Clone, Copy, PartialEq, Eq)]
589pub enum TextConflict {
590 Overlap,
592 Binary,
594 TooLarge,
596}
597
598impl TextConflict {
599 pub fn phrase(self) -> &'static str {
601 match self {
602 TextConflict::Overlap => "both sides changed the same lines",
603 TextConflict::Binary => "both sides changed a binary file",
604 TextConflict::TooLarge => "both sides changed a file too large to merge by line",
605 }
606 }
607}
608
609#[derive(Debug, Clone, Copy, PartialEq, Eq)]
612struct Hunk {
613 b0: usize,
614 b1: usize,
615 s0: usize,
616 s1: usize,
617}
618
619fn split_keep(bytes: &[u8]) -> Vec<&[u8]> {
620 bytes.split_inclusive(|b| *b == b'\n').collect()
621}
622
623fn hunks(base: &[&[u8]], side: &[&[u8]]) -> Vec<Hunk> {
626 let mut start = 0;
627 while start < base.len() && start < side.len() && base[start] == side[start] {
628 start += 1;
629 }
630 let mut end = 0;
631 while end < base.len() - start
632 && end < side.len() - start
633 && base[base.len() - 1 - end] == side[side.len() - 1 - end]
634 {
635 end += 1;
636 }
637 let (bm, sm) = (
638 &base[start..base.len() - end],
639 &side[start..side.len() - end],
640 );
641 if bm.is_empty() && sm.is_empty() {
642 return Vec::new();
643 }
644 let mut out: Vec<Hunk> = Vec::new();
645 let (mut bi, mut si) = (0usize, 0usize);
646 let flush = |bi: usize, b_to: usize, si: usize, s_to: usize, out: &mut Vec<Hunk>| {
647 if bi < b_to || si < s_to {
648 out.push(Hunk {
649 b0: start + bi,
650 b1: start + b_to,
651 s0: start + si,
652 s1: start + s_to,
653 });
654 }
655 };
656 for op in crate::diff::diff_ops(bm, sm) {
657 if let crate::diff::Op::Equal { old, new, count } = op {
658 flush(bi, old, si, new, &mut out);
659 bi = old + count;
660 si = new + count;
661 }
662 }
663 flush(bi, bm.len(), si, sm.len(), &mut out);
664 out
665}
666
667pub fn merge_text(
674 base: &[u8],
675 ours: &[u8],
676 theirs: &[u8],
677) -> std::result::Result<Vec<u8>, TextConflict> {
678 if [base, ours, theirs]
679 .iter()
680 .any(|b| b.len() > MAX_TEXT_MERGE_BYTES)
681 {
682 return Err(TextConflict::TooLarge);
683 }
684 if [base, ours, theirs]
685 .iter()
686 .any(|b| crate::diff::is_binary(b))
687 {
688 return Err(TextConflict::Binary);
689 }
690 let bl = split_keep(base);
691 let ol = split_keep(ours);
692 let tl = split_keep(theirs);
693 let mut all: Vec<(u8, Hunk)> = hunks(&bl, &ol)
694 .into_iter()
695 .map(|h| (0u8, h))
696 .chain(hunks(&bl, &tl).into_iter().map(|h| (1u8, h)))
697 .collect();
698 all.sort_by_key(|(side, h)| (h.b0, h.b1, *side));
699 let sides = [&ol, &tl];
700 let mut out: Vec<u8> = Vec::with_capacity(ours.len().max(theirs.len()));
701 let mut pos = 0usize;
702 let mut i = 0usize;
703 while i < all.len() {
704 let lo = all[i].1.b0;
705 let mut hi = all[i].1.b1;
706 let mut j = i + 1;
708 while j < all.len() && all[j].1.b0 <= hi {
709 hi = hi.max(all[j].1.b1);
710 j += 1;
711 }
712 for line in &bl[pos..lo] {
713 out.extend_from_slice(line);
714 }
715 let group = &all[i..j];
716 let region = |side: u8| -> Option<(usize, usize)> {
717 let mine: Vec<&Hunk> = group
718 .iter()
719 .filter(|(s, _)| *s == side)
720 .map(|(_, h)| h)
721 .collect();
722 let first = mine.first()?;
723 let last = mine.last()?;
724 Some((first.s0 - (first.b0 - lo), last.s1 + (hi - last.b1)))
725 };
726 match (region(0), region(1)) {
727 (Some((s0, s1)), None) => {
728 for line in &sides[0][s0..s1] {
729 out.extend_from_slice(line);
730 }
731 }
732 (None, Some((s0, s1))) => {
733 for line in &sides[1][s0..s1] {
734 out.extend_from_slice(line);
735 }
736 }
737 (Some((o0, o1)), Some((t0, t1))) => {
738 if sides[0][o0..o1] != sides[1][t0..t1] {
739 return Err(TextConflict::Overlap);
740 }
741 for line in &sides[0][o0..o1] {
742 out.extend_from_slice(line);
743 }
744 }
745 (None, None) => unreachable!("a group holds at least one hunk"),
746 }
747 pos = hi;
748 i = j;
749 }
750 for line in &bl[pos..] {
751 out.extend_from_slice(line);
752 }
753 Ok(out)
754}
755
756#[derive(Debug, Clone, PartialEq, Eq)]
760pub struct Ident {
761 pub name: String,
763 pub email: String,
765}
766
767impl Ident {
768 pub fn line(&self) -> String {
770 let clean = |s: &str| -> String {
771 s.chars()
772 .filter(|c| !matches!(c, '<' | '>' | '\n' | '\0'))
773 .collect::<String>()
774 .trim()
775 .to_owned()
776 };
777 format!("{} <{}>", clean(&self.name), clean(&self.email))
778 }
779}
780
781pub fn commit_bytes(
783 tree: &ObjectId,
784 parents: &[ObjectId],
785 author: &Ident,
786 committer: &Ident,
787 when_unix: i64,
788 message: &str,
789) -> Vec<u8> {
790 let mut body = format!("tree {}\n", hex(tree));
791 for parent in parents {
792 body.push_str(&format!("parent {}\n", hex(parent)));
793 }
794 body.push_str(&format!("author {} {when_unix} +0000\n", author.line()));
795 body.push_str(&format!(
796 "committer {} {when_unix} +0000\n\n",
797 committer.line()
798 ));
799 body.push_str(message.trim_end());
800 body.push('\n');
801 body.into_bytes()
802}
803
804pub fn sign_commit(
809 bytes: &[u8],
810 hash: HashKind,
811 key: &gunnar_core::ssh_key::PrivateKey,
812) -> Result<Vec<u8>> {
813 let header = match hash {
814 HashKind::Sha256 => "gpgsig-sha256",
815 _ => "gpgsig",
816 };
817 let end = bytes
818 .windows(2)
819 .position(|w| w == b"\n\n")
820 .ok_or_else(|| err("the commit to sign has no end of headers"))?;
821 if bytes[..end]
822 .split(|&b| b == b'\n')
823 .any(|line| line.starts_with(b"gpgsig ") || line.starts_with(b"gpgsig-sha256 "))
824 {
825 return Err(err("the commit to sign is already signed"));
826 }
827 let signature = key
828 .sign(
829 gunnar_policy::signature::GIT_NAMESPACE,
830 gunnar_core::ssh_key::HashAlg::Sha512,
831 bytes,
832 )
833 .map_err(|e| err(format!("signing the commit: {e}")))?;
834 let armor = signature
835 .to_pem(gunnar_core::ssh_key::LineEnding::LF)
836 .map_err(|e| err(format!("armouring the commit's signature: {e}")))?;
837 let mut folded = header.to_owned();
838 for (i, line) in armor.trim_end_matches('\n').split('\n').enumerate() {
839 folded.push_str(if i == 0 { " " } else { "\n " });
840 folded.push_str(line);
841 }
842 folded.push('\n');
843 let mut out = Vec::with_capacity(bytes.len() + folded.len());
844 out.extend_from_slice(&bytes[..=end]);
845 out.extend_from_slice(folded.as_bytes());
846 out.extend_from_slice(&bytes[end + 1..]);
847 Ok(out)
848}
849
850impl NewObjects {
851 pub fn add_commit(&mut self, hash: HashKind, bytes: Vec<u8>) -> Result<ObjectId> {
853 self.add(hash, ObjType::Commit, bytes)
854 }
855}
856
857pub struct Overlay<'a> {
866 inner: &'a dyn GitServe,
867 new: &'a NewObjects,
868}
869
870impl<'a> Overlay<'a> {
871 pub fn new(inner: &'a dyn GitServe, new: &'a NewObjects) -> Self {
873 Overlay { inner, new }
874 }
875}
876
877fn read_only<T>() -> anyhow::Result<T> {
878 anyhow::bail!("the merge overlay is read-only: nothing is written before every gate has run")
879}
880
881impl GitOps for Overlay<'_> {
882 fn put(&self, _pack: &[u8], _refs: &[RefUpdate]) -> anyhow::Result<TxId> {
883 read_only()
884 }
885 fn put_pack(&self, _bytes: &[u8]) -> anyhow::Result<TxId> {
886 read_only()
887 }
888 fn put_refs(&self, _updates: &[RefUpdate]) -> anyhow::Result<TxId> {
889 read_only()
890 }
891 fn get(&self, oid: gst::Oid<'_>) -> anyhow::Result<Option<Stored>> {
892 self.inner.get(oid)
893 }
894 fn has(&self, oid: gst::Oid<'_>) -> anyhow::Result<bool> {
895 Ok(self.new.get(oid).is_some() || self.inner.has(oid)?)
896 }
897 fn size(&self, oid: gst::Oid<'_>) -> anyhow::Result<Option<u64>> {
898 match self.new.get(oid) {
899 Some((_, bytes)) => Ok(Some(bytes.len() as u64)),
900 None => self.inner.size(oid),
901 }
902 }
903 fn extents(&self, oids: &[gst::Oid<'_>]) -> anyhow::Result<Vec<Option<gst::Extent>>> {
904 self.inner.extents(oids)
905 }
906 fn refs(&self) -> anyhow::Result<Vec<RefRow>> {
907 self.inner.refs()
908 }
909 fn update_ref(
910 &self,
911 _name: &str,
912 _old: Option<gst::Oid<'_>>,
913 _new: Option<gst::Oid<'_>>,
914 ) -> anyhow::Result<TxId> {
915 read_only()
916 }
917 fn put_refs_cas(&self, _edits: &[RefCas<'_>]) -> anyhow::Result<TxId> {
918 read_only()
919 }
920 fn reachable(
921 &self,
922 want: &[gst::Oid<'_>],
923 have: &[gst::Oid<'_>],
924 ) -> anyhow::Result<Vec<Vec<u8>>> {
925 self.inner.reachable(want, have)
926 }
927 fn gc(&self) -> anyhow::Result<GcReport> {
928 read_only()
929 }
930}
931
932impl GitServe for Overlay<'_> {
933 fn read(&self, oid: gst::Oid<'_>) -> anyhow::Result<Option<(ObjType, Vec<u8>)>> {
934 match self.new.get(oid) {
935 Some((kind, bytes)) => Ok(Some((*kind, bytes.clone()))),
936 None => self.inner.read(oid),
937 }
938 }
939 fn header(&self, oid: gst::Oid<'_>) -> anyhow::Result<Option<(ObjType, u64)>> {
940 match self.new.get(oid) {
941 Some((kind, bytes)) => Ok(Some((*kind, bytes.len() as u64))),
942 None => self.inner.header(oid),
943 }
944 }
945 fn sizes(&self, oids: &[gst::Oid<'_>]) -> anyhow::Result<Vec<Option<u64>>> {
946 oids.iter().map(|oid| self.size(oid)).collect()
947 }
948 fn head(&self) -> anyhow::Result<Option<RefRow>> {
949 self.inner.head()
950 }
951 fn set_head(&self, _target: &str) -> anyhow::Result<TxId> {
952 read_only()
953 }
954 fn emit_pack(
955 &self,
956 objects: &[gst::Oid<'_>],
957 have: &[gst::Oid<'_>],
958 caps: &Caps,
959 out: &mut dyn std::io::Write,
960 ) -> anyhow::Result<PackStats> {
961 self.inner.emit_pack(objects, have, caps, out)
962 }
963 fn select(
964 &self,
965 want: &[gst::Oid<'_>],
966 have: &[gst::Oid<'_>],
967 ) -> anyhow::Result<Option<ReachSet>> {
968 self.inner.select(want, have)
969 }
970}
971
972pub fn subjects(git: &dyn GitServe, hash: HashKind, commits: &[ObjectId]) -> Vec<String> {
974 let mut out = Vec::new();
975 let mut seen = HashSet::new();
976 for id in commits {
977 if !seen.insert(*id) {
978 continue;
979 }
980 let Ok(bytes) = read_as(git, id, ObjType::Commit) else {
981 continue;
982 };
983 let Ok(commit) = object::commit(&bytes, hash) else {
984 continue;
985 };
986 let subject = String::from_utf8_lossy(&commit.message)
987 .lines()
988 .next()
989 .unwrap_or("")
990 .trim()
991 .to_owned();
992 out.push(subject);
993 }
994 out
995}
996
997#[cfg(test)]
998mod tests;