1use quarb::{AstAdapter, NodeId, Value};
41use std::cell::RefCell;
42use std::collections::HashMap;
43use std::path::PathBuf;
44
45#[derive(Debug, thiserror::Error)]
47pub enum GitError {
48 #[error("git: {0}")]
49 Git(String),
50 #[error("git: running git: {0}")]
51 Spawn(#[from] std::io::Error),
52}
53
54#[derive(Clone)]
56struct CommitInfo {
57 author: String,
58 email: String,
59 date: i64,
60 date_offset: Option<i16>,
63 committer: String,
64 subject: String,
65 message: String,
66 tree: String,
67 parents: Vec<String>,
68}
69
70#[derive(Clone)]
72enum Kind {
73 Root,
74 Dir(&'static str),
76 Ref {
78 name: String,
79 commit: String,
80 },
81 Commit(String),
82 Entry {
84 name: String,
85 oid: String,
86 entry_type: String, mode: String,
88 },
89}
90
91struct Node {
92 kind: Kind,
93 parent: Option<NodeId>,
94 children: RefCell<Option<Vec<NodeId>>>,
95}
96
97pub struct GitAdapter {
99 repo: PathBuf,
100 decoding: quarb::source::SourceDecoding,
103 nodes: RefCell<Vec<Node>>,
104 commits: RefCell<HashMap<String, CommitInfo>>,
105 commit_nodes: RefCell<HashMap<String, NodeId>>,
107 enumerated: RefCell<bool>,
109 changed: RefCell<HashMap<String, Vec<String>>>,
111 tag_map: RefCell<Option<HashMap<String, Vec<String>>>>,
113 children_of: RefCell<Option<HashMap<String, Vec<String>>>>,
117 blobs: RefCell<Option<Batch>>,
121 sizes: RefCell<Option<Batch>>,
122}
123
124struct Batch {
126 child: std::process::Child,
127 stdin: std::process::ChildStdin,
128 out: std::io::BufReader<std::process::ChildStdout>,
129}
130
131impl Batch {
132 fn spawn(repo: &std::path::Path, check: bool) -> std::io::Result<Batch> {
133 let mut child = std::process::Command::new("git")
134 .arg("-C")
135 .arg(repo)
136 .args(["cat-file", if check { "--batch-check" } else { "--batch" }])
137 .stdin(std::process::Stdio::piped())
138 .stdout(std::process::Stdio::piped())
139 .stderr(std::process::Stdio::null())
140 .spawn()?;
141 let stdin = child.stdin.take().expect("piped");
142 let out = std::io::BufReader::new(child.stdout.take().expect("piped"));
143 Ok(Batch { child, stdin, out })
144 }
145
146 fn header(&mut self, oid: &str) -> std::io::Result<Option<u64>> {
149 use std::io::{BufRead, Write};
150 writeln!(self.stdin, "{oid}")?;
151 self.stdin.flush()?;
152 let mut line = String::new();
153 self.out.read_line(&mut line)?;
154 let mut parts = line.split_whitespace();
155 let _oid = parts.next();
156 match (parts.next(), parts.next()) {
157 (Some("missing"), _) | (None, _) => Ok(None),
158 (Some(_), Some(size)) => Ok(size.parse().ok()),
159 _ => Ok(None),
160 }
161 }
162
163 fn body(&mut self, size: u64) -> std::io::Result<Vec<u8>> {
165 use std::io::Read;
166 let mut buf = vec![0u8; size as usize];
167 self.out.read_exact(&mut buf)?;
168 let mut nl = [0u8; 1];
169 self.out.read_exact(&mut nl)?;
170 Ok(buf)
171 }
172}
173
174impl Drop for GitAdapter {
175 fn drop(&mut self) {
176 for b in [self.blobs.get_mut(), self.sizes.get_mut()] {
177 if let Some(b) = b {
178 let _ = b.child.kill();
179 let _ = b.child.wait();
180 }
181 }
182 }
183}
184
185const ROOT: NodeId = NodeId(0);
186const BRANCHES: NodeId = NodeId(1);
187const TAGS: NodeId = NodeId(2);
188const COMMITS: NodeId = NodeId(3);
189
190impl GitAdapter {
191 pub fn open(path: &std::path::Path) -> Result<Self, GitError> {
193 Self::open_with_decoding(path, quarb::source::SourceDecoding::default())
194 }
195
196 pub fn open_with_decoding(
200 path: &std::path::Path,
201 decoding: quarb::source::SourceDecoding,
202 ) -> Result<Self, GitError> {
203 let adapter = GitAdapter {
204 repo: path.to_path_buf(),
205 decoding,
206 nodes: RefCell::new(vec![
207 Node {
208 kind: Kind::Root,
209 parent: None,
210 children: RefCell::new(None),
211 },
212 Node {
213 kind: Kind::Dir("branches"),
214 parent: Some(ROOT),
215 children: RefCell::new(None),
216 },
217 Node {
218 kind: Kind::Dir("tags"),
219 parent: Some(ROOT),
220 children: RefCell::new(None),
221 },
222 Node {
223 kind: Kind::Dir("commits"),
224 parent: Some(ROOT),
225 children: RefCell::new(None),
226 },
227 ]),
228 commits: RefCell::new(HashMap::new()),
229 commit_nodes: RefCell::new(HashMap::new()),
230 enumerated: RefCell::new(false),
231 changed: RefCell::new(HashMap::new()),
232 tag_map: RefCell::new(None),
233 children_of: RefCell::new(None),
234 blobs: RefCell::new(None),
235 sizes: RefCell::new(None),
236 };
237 adapter.git(&["rev-parse", "--git-dir"])?;
239 Ok(adapter)
240 }
241
242 pub fn locator(&self, node: NodeId) -> String {
245 let nodes = self.nodes.borrow();
246 let mut parts = Vec::new();
247 let mut cur = Some(node);
248 while let Some(n) = cur {
249 let nd = &nodes[n.0 as usize];
250 match &nd.kind {
251 Kind::Root => {}
252 Kind::Dir(d) => parts.push(d.to_string()),
253 Kind::Ref { name, .. } => parts.push(name.clone()),
254 Kind::Commit(h) => parts.push(h[..7.min(h.len())].to_string()),
255 Kind::Entry { name, .. } => parts.push(name.clone()),
256 }
257 cur = nd.parent;
258 }
259 parts.reverse();
260 format!("/{}", parts.join("/"))
261 }
262
263 fn git(&self, args: &[&str]) -> Result<String, GitError> {
270 let out = self.git_bytes(args)?;
271 String::from_utf8(out).map_err(|e| {
272 GitError::Git(format!(
273 "git {}: output is not UTF-8 at byte {}",
274 args.first().unwrap_or(&""),
275 e.utf8_error().valid_up_to()
276 ))
277 })
278 }
279
280 fn blob_bytes(&self, oid: &str) -> Result<Vec<u8>, GitError> {
283 let batched = (|| -> std::io::Result<Option<Vec<u8>>> {
284 let mut slot = self.blobs.borrow_mut();
285 if slot.is_none() {
286 *slot = Some(Batch::spawn(&self.repo, false)?);
287 }
288 let b = slot.as_mut().expect("spawned");
289 match b.header(oid)? {
290 Some(size) => b.body(size).map(Some),
291 None => Ok(None),
292 }
293 })();
294 match batched {
295 Ok(Some(bytes)) => Ok(bytes),
296 Ok(None) => Err(GitError::Git(format!("blob {oid}: missing"))),
297 Err(_) => {
298 *self.blobs.borrow_mut() = None;
299 self.git_bytes(&["cat-file", "blob", oid])
300 }
301 }
302 }
303
304 fn blob_size(&self, oid: &str) -> Option<u64> {
307 let batched = (|| -> std::io::Result<Option<u64>> {
308 let mut slot = self.sizes.borrow_mut();
309 if slot.is_none() {
310 *slot = Some(Batch::spawn(&self.repo, true)?);
311 }
312 slot.as_mut().expect("spawned").header(oid)
313 })();
314 match batched {
315 Ok(size) => size,
316 Err(_) => {
317 *self.sizes.borrow_mut() = None;
318 self.git(&["cat-file", "-s", oid])
319 .ok()
320 .and_then(|s| s.trim().parse().ok())
321 }
322 }
323 }
324
325 fn children_of(&self, hash: &str) -> Vec<String> {
328 self.enumerate_commits();
329 if self.children_of.borrow().is_none() {
330 let mut index: HashMap<String, Vec<String>> = HashMap::new();
331 for (h, info) in self.commits.borrow().iter() {
332 for p in &info.parents {
333 index.entry(p.clone()).or_default().push(h.clone());
334 }
335 }
336 for v in index.values_mut() {
337 v.sort();
338 }
339 *self.children_of.borrow_mut() = Some(index);
340 }
341 self.children_of
342 .borrow()
343 .as_ref()
344 .and_then(|i| i.get(hash).cloned())
345 .unwrap_or_default()
346 }
347
348 fn git_bytes(&self, args: &[&str]) -> Result<Vec<u8>, GitError> {
350 let out = std::process::Command::new("git")
351 .arg("-C")
352 .arg(&self.repo)
353 .args(["-c", "core.quotepath=off"])
354 .args(args)
355 .output()?;
356 if !out.status.success() {
357 return Err(GitError::Git(
358 String::from_utf8(out.stderr)
359 .map(|s| s.trim().to_string())
360 .unwrap_or_else(|_| "(git's message is not UTF-8)".to_string()),
361 ));
362 }
363 Ok(out.stdout)
364 }
365
366 fn push_node(&self, kind: Kind, parent: Option<NodeId>) -> NodeId {
367 let mut nodes = self.nodes.borrow_mut();
368 let id = NodeId(nodes.len() as u64);
369 nodes.push(Node {
370 kind,
371 parent,
372 children: RefCell::new(None),
373 });
374 id
375 }
376
377 fn commit_node(&self, hash: &str) -> NodeId {
379 if let Some(&id) = self.commit_nodes.borrow().get(hash) {
380 return id;
381 }
382 let id = self.push_node(Kind::Commit(hash.to_string()), Some(COMMITS));
383 self.commit_nodes.borrow_mut().insert(hash.to_string(), id);
384 id
385 }
386
387 fn commit_info(&self, hash: &str) -> Option<CommitInfo> {
389 if let Some(i) = self.commits.borrow().get(hash) {
390 return Some(i.clone());
391 }
392 let out = self
393 .git(&[
394 "show",
395 "-s",
396 "--encoding=UTF-8",
397 "--format=%an%x00%ae%x00%at%x00%cn%x00%s%x00%B%x00%T%x00%P%x00%ai",
398 hash,
399 ])
400 .ok()?;
401 let info = parse_info(&out)?;
402 self.commits
403 .borrow_mut()
404 .insert(hash.to_string(), info.clone());
405 Some(info)
406 }
407
408 fn commits_prefix(&self, k: usize) -> Vec<NodeId> {
415 let n = k.to_string();
416 let out = self
417 .git(&[
418 "rev-list",
419 "--encoding=UTF-8",
420 "--all",
421 "-n",
422 &n,
423 "--format=%an%x00%ae%x00%at%x00%cn%x00%s%x00%B%x00%T%x00%P%x00%ai%x1e",
424 ])
425 .unwrap_or_default();
426 let mut ids = Vec::new();
427 for (hash, body) in split_commit_records(&out) {
428 if let Some(info) = parse_info(body) {
429 self.commits.borrow_mut().insert(hash.to_string(), info);
430 }
431 ids.push(self.commit_node(hash));
432 }
433 ids
434 }
435
436 fn enumerate_commits(&self) -> Vec<NodeId> {
437 if let Some(c) = self.nodes.borrow()[COMMITS.0 as usize]
438 .children
439 .borrow()
440 .as_ref()
441 {
442 return c.clone();
443 }
444 let out = self
445 .git(&[
446 "rev-list",
447 "--encoding=UTF-8",
448 "--all",
449 "--format=%an%x00%ae%x00%at%x00%cn%x00%s%x00%B%x00%T%x00%P%x00%ai%x1e",
450 ])
451 .unwrap_or_default();
452 let mut ids = Vec::new();
453 for (hash, body) in split_commit_records(&out) {
454 if let Some(info) = parse_info(body) {
455 self.commits.borrow_mut().insert(hash.to_string(), info);
456 }
457 ids.push(self.commit_node(hash));
458 }
459 *self.enumerated.borrow_mut() = true;
460 *self.nodes.borrow()[COMMITS.0 as usize]
461 .children
462 .borrow_mut() = Some(ids.clone());
463 ids
464 }
465
466 fn tags_at(&self, hash: &str) -> Vec<String> {
469 if self.tag_map.borrow().is_none() {
470 let out = self
471 .git(&[
472 "for-each-ref",
473 "refs/tags",
474 "--format=%(refname:short)%00%(objectname)%00%(*objectname)",
475 ])
476 .unwrap_or_default();
477 let mut map: HashMap<String, Vec<String>> = HashMap::new();
478 for line in out.lines() {
479 let mut f = line.split('\u{0}');
480 let (Some(name), Some(oid)) = (f.next(), f.next()) else {
481 continue;
482 };
483 let peeled = f.next().filter(|p| !p.is_empty()).unwrap_or(oid);
484 map.entry(peeled.to_string())
485 .or_default()
486 .push(name.to_string());
487 }
488 *self.tag_map.borrow_mut() = Some(map);
489 }
490 self.tag_map
491 .borrow()
492 .as_ref()
493 .and_then(|m| m.get(hash).cloned())
494 .unwrap_or_default()
495 }
496
497 fn refs(&self, dir: NodeId, prefix: &str) -> Vec<NodeId> {
500 if let Some(c) = self.nodes.borrow()[dir.0 as usize]
501 .children
502 .borrow()
503 .as_ref()
504 {
505 return c.clone();
506 }
507 let out = self
508 .git(&[
509 "for-each-ref",
510 prefix,
511 "--format=%(refname:short)%00%(objectname)%00%(*objectname)",
512 ])
513 .unwrap_or_default();
514 let mut ids = Vec::new();
515 for line in out.lines() {
516 let mut f = line.split('\u{0}');
517 let (Some(name), Some(oid)) = (f.next(), f.next()) else {
518 continue;
519 };
520 let deref = f.next().unwrap_or("");
521 let commit = if deref.is_empty() { oid } else { deref };
522 ids.push(self.push_node(
523 Kind::Ref {
524 name: name.to_string(),
525 commit: commit.to_string(),
526 },
527 Some(dir),
528 ));
529 }
530 *self.nodes.borrow()[dir.0 as usize].children.borrow_mut() = Some(ids.clone());
531 ids
532 }
533
534 fn tree_children(&self, parent: NodeId, tree_oid: &str) -> Vec<NodeId> {
536 if let Some(c) = self.nodes.borrow()[parent.0 as usize]
537 .children
538 .borrow()
539 .as_ref()
540 {
541 return c.clone();
542 }
543 let out = self.git(&["ls-tree", "-z", tree_oid]).unwrap_or_default();
544 let mut ids = Vec::new();
545 for entry in out.split('\u{0}') {
549 let Some((meta, name)) = entry.split_once('\t') else {
550 continue;
551 };
552 let mut f = meta.split(' ');
553 let (Some(mode), Some(entry_type), Some(oid)) = (f.next(), f.next(), f.next()) else {
554 continue;
555 };
556 ids.push(self.push_node(
557 Kind::Entry {
558 name: name.to_string(),
559 oid: oid.to_string(),
560 entry_type: entry_type.to_string(),
561 mode: mode.to_string(),
562 },
563 Some(parent),
564 ));
565 }
566 *self.nodes.borrow()[parent.0 as usize].children.borrow_mut() = Some(ids.clone());
567 ids
568 }
569
570 fn changed_paths(&self, hash: &str) -> Vec<String> {
573 if let Some(c) = self.changed.borrow().get(hash) {
574 return c.clone();
575 }
576 let out = self
577 .git(&[
578 "diff-tree",
579 "--root",
580 "--no-commit-id",
581 "--name-only",
582 "-z",
583 "-r",
584 hash,
585 ])
586 .unwrap_or_default();
587 let paths: Vec<String> = out
590 .split('\u{0}')
591 .filter(|p| !p.is_empty())
592 .map(str::to_string)
593 .collect();
594 self.changed
595 .borrow_mut()
596 .insert(hash.to_string(), paths.clone());
597 paths
598 }
599
600 fn entry_context(&self, node: NodeId) -> Option<(String, String)> {
603 let nodes = self.nodes.borrow();
604 let mut parts = Vec::new();
605 let mut cur = Some(node);
606 while let Some(n) = cur {
607 let nd = &nodes[n.0 as usize];
608 match &nd.kind {
609 Kind::Entry { name, .. } => parts.push(name.clone()),
610 Kind::Commit(h) => {
611 parts.reverse();
612 return Some((h.clone(), parts.join("/")));
613 }
614 Kind::Ref { commit, .. } => {
615 parts.reverse();
616 return Some((commit.clone(), parts.join("/")));
617 }
618 _ => return None,
619 }
620 cur = nd.parent;
621 }
622 None
623 }
624
625 fn commit_of(&self, node: NodeId) -> Option<String> {
627 match &self.nodes.borrow()[node.0 as usize].kind {
628 Kind::Commit(h) => Some(h.clone()),
629 Kind::Ref { commit, .. } => Some(commit.clone()),
630 _ => None,
631 }
632 }
633
634 fn commit_ancestor(&self, node: NodeId) -> Option<String> {
637 let mut cur = Some(node);
638 while let Some(n) = cur {
639 if let Some(h) = self.commit_of(n) {
640 return Some(h);
641 }
642 cur = self.nodes.borrow()[n.0 as usize].parent;
643 }
644 None
645 }
646}
647
648fn split_commit_records(out: &str) -> impl Iterator<Item = (&str, &str)> {
655 out.split('\u{1e}').filter_map(|record| {
656 let rest = record.trim_start().strip_prefix("commit ")?;
657 let (hash, body) = rest.split_once('\n')?;
658 Some((hash.trim(), body))
659 })
660}
661
662fn parse_info(body: &str) -> Option<CommitInfo> {
663 let f: Vec<&str> = body.trim_end_matches('\n').split('\u{0}').collect();
664 if f.len() < 8 {
665 return None;
666 }
667 Some(CommitInfo {
668 author: f[0].to_string(),
669 email: f[1].to_string(),
670 date: f[2].parse().unwrap_or(0),
671 date_offset: f.get(8).and_then(|iso| {
672 let tail = iso.trim().rsplit(' ').next()?;
675 let sign = match tail.as_bytes().first()? {
676 b'+' => 1i16,
677 b'-' => -1i16,
678 _ => return None,
679 };
680 let h: i16 = tail.get(1..3)?.parse().ok()?;
681 let m: i16 = tail.get(3..5)?.parse().ok()?;
682 Some(sign * (h * 60 + m))
683 }),
684 committer: f[3].to_string(),
685 subject: f[4].to_string(),
686 message: f[5].trim_end().to_string(),
687 tree: f[6].to_string(),
688 parents: f[7].split_whitespace().map(str::to_string).collect(),
689 })
690}
691
692impl AstAdapter for GitAdapter {
693 fn root(&self) -> NodeId {
694 ROOT
695 }
696
697 fn select(&self, from: NodeId, plan: &quarb::StepPlan<'_>) -> Option<quarb::Selection> {
701 let k = plan.limit?;
702 if from != COMMITS
703 || plan.axis != quarb::PlanAxis::Child
704 || plan.name.is_some()
705 || !plan.conjuncts.is_empty()
706 || !plan.traits.is_empty()
707 || *self.enumerated.borrow()
708 {
709 return None;
710 }
711 Some(quarb::Selection {
712 nodes: self
713 .commits_prefix(k)
714 .into_iter()
715 .map(|id| (id, 1))
716 .collect(),
717 applied: Vec::new(),
718 })
719 }
720
721 fn children(&self, node: NodeId) -> Vec<NodeId> {
722 let kind = self.nodes.borrow()[node.0 as usize].kind.clone();
723 match kind {
724 Kind::Root => {
725 if self.nodes.borrow()[ROOT.0 as usize]
728 .children
729 .borrow()
730 .is_none()
731 {
732 let mut ids = vec![BRANCHES, TAGS, COMMITS];
733 if let Ok(h) = self.git(&["rev-parse", "HEAD"]) {
734 ids.push(self.push_node(
735 Kind::Ref {
736 name: "HEAD".to_string(),
737 commit: h.trim().to_string(),
738 },
739 Some(ROOT),
740 ));
741 }
742 *self.nodes.borrow()[ROOT.0 as usize].children.borrow_mut() = Some(ids);
743 }
744 self.nodes.borrow()[ROOT.0 as usize]
745 .children
746 .borrow()
747 .clone()
748 .unwrap_or_default()
749 }
750 Kind::Dir("branches") => self.refs(BRANCHES, "refs/heads"),
751 Kind::Dir("tags") => self.refs(TAGS, "refs/tags"),
752 Kind::Dir(_) => self.enumerate_commits(),
753 Kind::Ref { commit, .. } | Kind::Commit(commit) => {
754 let Some(info) = self.commit_info(&commit) else {
755 return Vec::new();
756 };
757 self.tree_children(node, &info.tree)
758 }
759 Kind::Entry {
760 oid, entry_type, ..
761 } => {
762 if entry_type == "tree" {
763 self.tree_children(node, &oid)
764 } else {
765 Vec::new()
766 }
767 }
768 }
769 }
770
771 fn children_named(&self, node: NodeId, name: &str) -> Vec<NodeId> {
775 if node == COMMITS {
776 if let Some(&id) = self.commit_nodes.borrow().get(name) {
780 return vec![id];
781 }
782 let Ok(out) = self.git(&[
783 "rev-parse",
784 "--verify",
785 "--quiet",
786 &format!("{name}^{{commit}}"),
787 ]) else {
788 return Vec::new();
789 };
790 return vec![self.commit_node(out.trim())];
791 }
792 self.children(node)
793 .into_iter()
794 .filter(|&c| self.name(c).as_deref() == Some(name))
795 .collect()
796 }
797
798 fn name(&self, node: NodeId) -> Option<String> {
799 match &self.nodes.borrow()[node.0 as usize].kind {
800 Kind::Root => None,
801 Kind::Dir(d) => Some(d.to_string()),
802 Kind::Ref { name, .. } => Some(name.clone()),
803 Kind::Commit(h) => Some(h.clone()),
804 Kind::Entry { name, .. } => Some(name.clone()),
805 }
806 }
807
808 fn parent(&self, node: NodeId) -> Option<NodeId> {
809 self.nodes.borrow()[node.0 as usize].parent
810 }
811
812 fn traits(&self, node: NodeId) -> Vec<String> {
816 let nodes = self.nodes.borrow();
817 let t = match &nodes[node.0 as usize].kind {
818 Kind::Root => "repository",
819 Kind::Dir(_) => "dir",
820 Kind::Commit(_) => "commit",
821 Kind::Ref { .. } => match nodes[node.0 as usize].parent {
822 Some(TAGS) => "tag",
823 Some(BRANCHES) => "branch",
824 _ => "commit",
825 },
826 Kind::Entry { entry_type, .. } => {
827 let base = if entry_type == "tree" { "tree" } else { "blob" };
828 let mut out = vec![base.to_string()];
829 drop(nodes);
830 if let Some((hash, path)) = self.entry_context(node) {
834 let prefix = format!("{path}/");
835 if self
836 .changed_paths(&hash)
837 .iter()
838 .any(|p| *p == path || p.starts_with(&prefix))
839 {
840 out.push("changed".to_string());
841 }
842 }
843 return out;
844 }
845 };
846 vec![t.to_string()]
847 }
848
849 fn property(&self, node: NodeId, name: &str) -> Option<Value> {
851 let hash = self.commit_of(node)?;
852 let info = self.commit_info(&hash)?;
853 Some(match name {
854 "author" => Value::Str(info.author),
855 "email" => Value::Str(info.email),
856 "date" => Value::Instant {
857 secs: info.date,
858 nanos: 0,
859 offset_min: info.date_offset,
860 calendar: Default::default(),
861 },
862 "committer" => Value::Str(info.committer),
863 "subject" => Value::Str(info.subject),
864 "message" => Value::Str(info.message),
865 "tree" => Value::Str(info.tree),
866 "hash" => Value::Str(hash),
867 "parent" => Value::Str(info.parents.first()?.clone()),
868 "changed" => Value::list(
871 self.changed_paths(&hash)
872 .into_iter()
873 .map(Value::Str)
874 .collect(),
875 ),
876 _ => return None,
877 })
878 }
879
880 fn provenance(&self, node: NodeId) -> quarb::Provenance {
887 let instant = self
888 .commit_ancestor(node)
889 .and_then(|h| self.commit_info(&h))
890 .map(|info| (info.date, 0, info.date_offset));
891 quarb::Provenance {
892 source: Some(self.repo.display().to_string()),
893 path: None,
894 instant_from: instant.map(|_| quarb::InstantFrom::Node),
895 instant,
896 dpid: None,
897 }
898 }
899
900 fn default_value(&self, node: NodeId) -> Option<Value> {
902 let (oid, is_blob) = match &self.nodes.borrow()[node.0 as usize].kind {
903 Kind::Entry {
904 oid, entry_type, ..
905 } => (oid.clone(), entry_type == "blob"),
906 _ => return None,
907 };
908 if !is_blob {
909 return None;
910 }
911 self.blob_bytes(&oid)
914 .ok()
915 .and_then(|bytes| self.decoding.decode(&oid, &bytes).ok())
916 .map(|d| Value::Str(d.text))
917 }
918
919 fn aliased_metadata(&self, _node: NodeId) -> &'static [&'static str] {
930 &[
931 "short",
932 "hash",
933 "n-parents",
934 "n-changed",
935 "tags",
936 "n-tags",
937 "type",
938 "mode",
939 "size",
940 ]
941 }
942
943 fn metadata_catalogue(&self) -> &'static [&'static str] {
948 &[
949 "short",
950 "n-parents",
951 "n-changed",
952 "tags",
953 "n-tags",
954 "type",
955 "mode",
956 "hash",
957 "size",
958 ]
959 }
960
961 fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
962 if let Some(hash) = self.commit_of(node) {
963 return match key {
964 "short" => Some(Value::Str(hash[..7.min(hash.len())].to_string())),
965 "n-parents" => Some(Value::Int(self.commit_info(&hash)?.parents.len() as i64)),
966 "n-changed" => Some(Value::Int(self.changed_paths(&hash).len() as i64)),
967 "tags" => Some(Value::list(
968 self.tags_at(&hash).into_iter().map(Value::Str).collect(),
969 )),
970 "n-tags" => Some(Value::Int(self.tags_at(&hash).len() as i64)),
971 _ => None,
972 };
973 }
974 let (oid, entry_type, mode) = match &self.nodes.borrow()[node.0 as usize].kind {
975 Kind::Entry {
976 oid,
977 entry_type,
978 mode,
979 ..
980 } => (oid.clone(), entry_type.clone(), mode.clone()),
981 _ => return None,
982 };
983 match key {
984 "type" => Some(Value::Str(entry_type)),
985 "mode" => Some(Value::Str(mode)),
986 "hash" => Some(Value::Str(oid)),
987 "size" => self.blob_size(&oid).map(|n| Value::bytes(n as i64)),
988 _ => None,
989 }
990 }
991
992 fn resolve(&self, node: NodeId, property: &str, _hint: Option<&str>) -> Option<NodeId> {
995 if property != "parent" {
996 return None;
997 }
998 let hash = self.commit_of(node)?;
999 let first = self.commit_info(&hash)?.parents.first()?.clone();
1000 Some(self.commit_node(&first))
1001 }
1002
1003 fn acyclic_link(&self, label: &str) -> Option<String> {
1008 (label == "parent")
1009 .then(|| "the commit history (a commit's parents precede it)".to_string())
1010 }
1011
1012 fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
1014 let Some(hash) = self.commit_of(node) else {
1015 return Vec::new();
1016 };
1017 let Some(info) = self.commit_info(&hash) else {
1018 return Vec::new();
1019 };
1020 info.parents
1021 .iter()
1022 .map(|p| ("parent".to_string(), self.commit_node(p)))
1023 .collect()
1024 }
1025
1026 fn ref_property(&self, node: NodeId) -> Option<String> {
1029 self.commit_of(node).map(|_| "parent".to_string())
1030 }
1031
1032 fn reverse_resolve(
1038 &self,
1039 node: NodeId,
1040 property: Option<&str>,
1041 _hint: Option<&str>,
1042 ) -> Option<Vec<NodeId>> {
1043 let hash = self.commit_of(node)?;
1044 if property.is_some_and(|p| p != "parent") {
1046 return Some(Vec::new());
1047 }
1048 let children: Vec<String> = self
1052 .children_of(&hash)
1053 .into_iter()
1054 .filter(|h| {
1055 self.commit_info(h)
1056 .is_some_and(|i| i.parents.first() == Some(&hash))
1057 })
1058 .collect();
1059 Some(children.iter().map(|h| self.commit_node(h)).collect())
1060 }
1061
1062 fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
1065 let Some(hash) = self.commit_of(node) else {
1066 return Vec::new();
1067 };
1068 self.children_of(&hash)
1069 .into_iter()
1070 .map(|h| ("parent".to_string(), self.commit_node(&h)))
1071 .collect()
1072 }
1073}
1074
1075#[cfg(test)]
1076mod tests {
1077 use super::*;
1078
1079 #[test]
1083 fn record_split_survives_commit_word_in_body() {
1084 let out = "commit aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
1085Ann\u{0}ann@example\u{0}1000\u{0}Ann\u{0}\
1086Revert commit deadbeef\u{0}\
1087Revert commit deadbeef\n\nThis reverts commit deadbeef.\u{0}\
1088tttttttttttttttttttttttttttttttttttttttt\u{0}\
1089pppppppppppppppppppppppppppppppppppppppp\u{0}\
10902026-07-15 12:00:00 +0100\u{1e}\n\
1091commit bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n\
1092Bo\u{0}bo@example\u{0}2000\u{0}Bo\u{0}second\u{0}second\u{0}\
1093tttttttttttttttttttttttttttttttttttttttt\u{0}\u{0}\
10942026-07-15 13:00:00 +0100\u{1e}\n";
1095 let recs: Vec<(&str, &str)> = split_commit_records(out).collect();
1096 assert_eq!(recs.len(), 2);
1097 assert_eq!(recs[0].0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
1098 let a = parse_info(recs[0].1).expect("first record parses");
1099 assert_eq!(a.author, "Ann");
1100 assert_eq!(a.subject, "Revert commit deadbeef");
1101 assert_eq!(
1102 a.message,
1103 "Revert commit deadbeef\n\nThis reverts commit deadbeef."
1104 );
1105 assert_eq!(recs[1].0, "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
1106 let b = parse_info(recs[1].1).expect("second record parses");
1107 assert_eq!(b.subject, "second");
1108 }
1109}