crates/kernel/ents-anchor/src/lib.rs
| 1 | //! Anchor storage and projection: durable pointers into source — a blob, |
| 2 | //! an optional line range, and a specific commit — and their read-time |
| 3 | //! projection onto any other commit. |
| 4 | //! |
| 5 | //! This crate owns the `Anchor` abstraction from `docs/spec/anchor.sdoc` |
| 6 | //! (overview.sdoc abstraction 3). Anchors resolve and project independently |
| 7 | //! of any consumer: `ents-forge`'s `Comment` is merely the first client |
| 8 | //! (its `anchor: RawTree` field embeds the tree an [`Anchor`] serializes |
| 9 | //! to), and reviews, TODO trackers, and blame overlays can reuse the same |
| 10 | //! mechanism. (`ents-forge` depends on this crate, not the other way |
| 11 | //! around, so this crate's own examples and tests stand a `Comment`-shaped |
| 12 | //! struct in for it rather than importing it.) |
| 13 | //! |
| 14 | //! # Spec coverage |
| 15 | //! |
| 16 | //! This crate implements, from `docs/spec/anchor.sdoc`: |
| 17 | //! |
| 18 | //! - `anchor.definition` — [`Anchor`] and [`capture`]'s validation. |
| 19 | //! - `anchor.immutable` — no mutating API exists; [`snippet`] derives the |
| 20 | //! anchored text at read time; the commit id is plain data. |
| 21 | //! - `anchor.retention` — [`Anchor::content`] and [`Anchor::context`] are |
| 22 | //! ordinary blob tree entries in the anchor's own serialized tree, never |
| 23 | //! a gitlink. |
| 24 | //! - `anchor.projection` — [`project`] / [`project_exact`] and the |
| 25 | //! four-outcome [`Projection`] taxonomy. |
| 26 | //! - `anchor.fuzzy-fallback` — [`project_from_context`], which [`project`] |
| 27 | //! degrades to once the anchored commit has been garbage collected. |
| 28 | //! - `anchor.working-tree` — [`capture_worktree`] (the on-disk bytes as a |
| 29 | //! capture source, `HEAD` recorded as the best-effort commit field) and |
| 30 | //! [`project_worktree`] (the on-disk bytes, or a caller-supplied buffer |
| 31 | //! standing in for them, as a projection target). |
| 32 | //! |
| 33 | //! # Examples |
| 34 | //! |
| 35 | //! Capture an anchor, store it inside a `Comment`, read it back, and |
| 36 | //! project it onto a later commit: |
| 37 | //! |
| 38 | //! ``` |
| 39 | //! use ents_anchor::{Anchor, LineRange, Projection}; |
| 40 | //! use facet_git_tree::RawTree; |
| 41 | //! |
| 42 | //! // Stands in for `ents-forge`'s `Comment` (this crate cannot |
| 43 | //! // depend on `ents-forge`, which itself depends on this crate): any |
| 44 | //! // struct embedding an anchor's tree by `RawTree` behaves identically. |
| 45 | //! # #[derive(facet::Facet)] |
| 46 | //! # struct Comment { body: String, anchor: RawTree } |
| 47 | //! # |
| 48 | //! # fn git(dir: &std::path::Path, args: &[&str]) { |
| 49 | //! # let status = std::process::Command::new("git").arg("-C").arg(dir) |
| 50 | //! # .args(["-c", "user.name=t", "-c", "user.email=t@example.com"]) |
| 51 | //! # .args(args).status().unwrap(); |
| 52 | //! # assert!(status.success()); |
| 53 | //! # } |
| 54 | //! # let dir = tempfile::tempdir().expect("tempdir"); |
| 55 | //! # std::process::Command::new("git").arg("init").arg("-q").arg(dir.path()).status().unwrap(); |
| 56 | //! # std::fs::write(dir.path().join("file.txt"), (1..=10).map(|n| format!("line {n}\n")).collect::<String>()).unwrap(); |
| 57 | //! # git(dir.path(), &["add", "-A"]); |
| 58 | //! # git(dir.path(), &["commit", "-q", "-m", "one"]); |
| 59 | //! let repo = gix::open(dir.path()).expect("open"); |
| 60 | //! |
| 61 | //! // Capture against HEAD: commit, path, blob, and range are validated |
| 62 | //! // and recorded; content and context are embedded (`anchor.retention`). |
| 63 | //! let anchor = ents_anchor::capture(&repo, "HEAD", "file.txt", Some(LineRange { start: 3, end: 4 })) |
| 64 | //! .expect("capture"); |
| 65 | //! assert_eq!(ents_anchor::snippet(&anchor).expect("snippet"), "line 3\nline 4\n"); |
| 66 | //! |
| 67 | //! // The anchor serializes into the same store the comment does; the |
| 68 | //! // comment embeds it by tree id (`RawTree`), so the anchored content is |
| 69 | //! // reachable from the comment's own ref. |
| 70 | //! let store = facet_git_tree::ObjectStore::default(); |
| 71 | //! let anchor_tree = facet_git_tree::serialize_into(&anchor, &store).expect("serialize anchor"); |
| 72 | //! let comment = Comment { |
| 73 | //! body: "these two lines look off by one".to_owned(), |
| 74 | //! anchor: RawTree::new(anchor_tree), |
| 75 | //! }; |
| 76 | //! let root = facet_git_tree::serialize_into(&comment, &store).expect("serialize comment"); |
| 77 | //! |
| 78 | //! // Read the comment back and recover the identical anchor. |
| 79 | //! let back: Comment = facet_git_tree::deserialize(&root, &store).expect("deserialize comment"); |
| 80 | //! let anchor_back: Anchor = |
| 81 | //! facet_git_tree::deserialize(&back.anchor.oid(), &store).expect("deserialize anchor"); |
| 82 | //! assert_eq!(anchor_back, anchor); |
| 83 | //! |
| 84 | //! // Edit above the range and project: the anchor relocates, unmutated. |
| 85 | //! # std::fs::write(dir.path().join("file.txt"), format!("added\n{}", (1..=10).map(|n| format!("line {n}\n")).collect::<String>())).unwrap(); |
| 86 | //! # git(dir.path(), &["add", "-A"]); |
| 87 | //! # git(dir.path(), &["commit", "-q", "-m", "two"]); |
| 88 | //! let repo = gix::open(dir.path()).expect("reopen"); |
| 89 | //! assert_eq!( |
| 90 | //! ents_anchor::project(&repo, &anchor_back, "HEAD").expect("project"), |
| 91 | //! Projection::Relocated { |
| 92 | //! path: "file.txt".to_owned(), |
| 93 | //! lines: Some(LineRange { start: 4, end: 5 }), |
| 94 | //! } |
| 95 | //! ); |
| 96 | //! ``` |
| 97 | |
| 98 | mod anchor; |
| 99 | mod binding; |
| 100 | mod error; |
| 101 | #[cfg(test)] |
| 102 | mod fixture; |
| 103 | mod projection; |
| 104 | mod util; |
| 105 | |
| 106 | pub use anchor::{Anchor, LineRange, capture, capture_worktree, snippet}; |
| 107 | pub use binding::{Binding, EvalState, Validity, revalidate}; |
| 108 | pub use error::{Error, Result}; |
| 109 | pub use projection::{Projection, project, project_exact, project_from_context, project_worktree}; |