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